Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

style props

variants { base { Bg = Surface; P = 4; BorderW = 1; Border = Border; } } — the closed set of visual props a variant assigns

Inside a `variants` block, each `Name = value` is a style prop from a fixed vocabulary the renderer maps to CSS — paint (`Bg`, `Color`, `Border`), borders (`BorderW`, `BorderStyle`), spacing (`P`, `Gap`), size (`W`, `Grow`), and more. A value is a number, a keyword, or a theme token by name. NOT on this page: `align`, `justify` and `gap` are LAYOUT ARGUMENTS passed to `Stack`/`Row`/`Box`, not style props — `osy docs ui-layout` has all three.

stable5 examples compiled by CIuistylingvariants

Summary#

A component's appearance lives in its variants block. Each leaf assignment there — Bg = Surface;, P = 4;, BorderW = 1; — is a style prop: a name from a fixed vocabulary that the renderer maps to one or more CSS properties. The vocabulary is closed and checked at compile time, so a misspelled prop (Backgroud = …) is an error, not a declaration that silently styles nothing.

component Card() {
  variants {
    base {
      Bg = Surface;         // paint
      P = 4;                // spacing (a step on the 0.25rem scale)
      Rounded = Card;       // a theme radius token
      BorderW = 1;          // a 1px border...
      Border = Border;      // ...painted with the theme's Border color
      Shadow = "0 1px 3px rgba(0,0,0,0.08)";   // a CSS box-shadow, or a theme Shadow token by name
    }
  }
  render { Stack { Slot; } }
}

Which token names do I actually have? osy kit tokens prints every token the starter theme ships — by group, with its value. (osy model --json reports the tokens your own theme blocks declare.) This page answers which style props exist; those answer which values they may take, and the two are different questions.

A style prop's value is one of four things: a number (lowered to the prop's unit — a spacing step, a pixel length, or a raw number), a keyword from that prop's closed set (Display = Display.Flex), a theme tokens token referenced by name (Bg = Surface), or a literal CSS string for a prop whose value is passed through verbatim (Shadow, Cols, …) — the per-prop tables below give each prop's value forms. A theme group is never a prerequisite: an app with no Shadow { } group can still write Shadow = "0 1px 3px rgba(0,0,0,0.08)". Reach for a token when the value is part of the app's design language and should change with the theme; reach for a literal for a one-off. Layout arguments (align, justify, gap) are separate — see layout primitives.

Signature#

variants {
  base {
    <StyleProp> = <number | keyword | tokenName>;
    ...
  }
}

Description#

The style-prop vocabulary is grouped by what it controls. Values are a number, a keyword (closed set), or a theme token name.

Which element a variants block styles — every top-level one#

A variants block styles the component's render root, and a component whose render has several top-level elements has several roots. All of them wear the styling — whichever is on screen:

component Divider(bool vertical = false) {
  variants { base { Bg = Border; } }        // ← applies to whichever Box renders
  render {
    if (vertical)  { Box(w: "1px",  h: "100%"); }
    if (!vertical) { Box(w: "100%", h: "1px");  }
  }
}

That is the rule for every way an element can reach the top level: written there directly, inside an if arm, or one per row of a top-level foreach. It is not inherited downward — anything nested inside a root is ordinary content, and styles itself.

A component whose render root is another component passes the styling on: the child's own roots wear it, on top of whatever the child's own variants block declares.

Two roots on screen at once — warning VARIANTS_ON_SEVERAL_ROOTS

Exclusive arms are what the rule is for. Two roots that can be on screen together almost never are: an overlay or click-catcher drawn beside a page-sized root takes the root's MinH = Length.Screen; Bg = … and becomes an opaque sheet over the whole window. So the compiler warns — osy validate prints it — naming the component, its styling, and each element that would wear it (with its line, the component it is drawn in, and its condition):

component Shell() {
  bool menuOpen = false;
  action Close() { menuOpen = false; }
  variants { base { MinH = Length.Screen; Bg = Colors.Bg; } }
  render { Frame(menuOpen, Close); }       // the frame's roots are the shell's roots
}
component Frame(bool menuOpen, Action onClose) {
  render {
    Stack(gap: 0) { Slot; }
    if (menuOpen) { Box(onClick: onClose); }   // ⚠ beside the root: a second root, while the menu is open
  }
}

The fix is always the same: put them inside one root element, so the styling lands once. A fixed-position overlay is unaffected by where in the tree it is drawn.

theme App {
  Colors { Surface = "#ffffff"; }
}

component Shell() {
  bool menuOpen = false;
  action Close() { menuOpen = false; }
  variants { base { MinH = Length.Screen; Bg = Colors.Surface; } }
  render { Frame(menuOpen, Close); }
}
component Frame(bool menuOpen, Action onClose) {
  render {
    Stack(gap: 0) {
      Slot;
      if (menuOpen) { Box(onClick: onClose); }
    }
  }
}

Two arms the compiler can see are exclusive are not warned: one if … else if … else chain, or bare ifs whose conditions contradict (c / !c, x == A / x == B, n > 0 / n <= 0). If two roots are never on screen together for a reason it cannot see, write them as one if … else.

What color is it — background, text, shadow, opacity#

Every row below that says color token takes one of these — the starter theme's Colors, in scope wherever using Osysharp.Ui; is:

Colors.Bg · OnBg · Surface · OnSurface · Border · BorderDanger · Muted · TextMuted · TextSecondary · Primary · OnPrimary · Danger · Success · Warning · Scrim · Transparent

Primary, Danger, Success and Warning are built with Palette.From(...), so each is a whole RAMP rather than one colour. A step is reached by a semantic alias — Subtle · Muted · Default · Hover · Active · Strong (Bg = Colors.Primary.Hover) — or by number: 50 · 100 · 200 · 300 · 400 · 500 · 600 · 700 · 800 · 900 · 950 (Colors.Primary[600]). The plain name is the base.

An app that declares its own theme adds to this set rather than replacing it, so osy model --json reports the tokens THIS app actually has, light and dark separately — the answer whenever the list above is not the whole of it.

PropCSSValue
Bgbackground-colorcolor token
Colorcolorcolor token
Borderborder-colorcolor token
Roundedborder-radiuslength / radius token
Shadowbox-shadowCSS value, or a shadow token — haloes the element's rectangle
TextShadowtext-shadowCSS value — a shadow on the glyphs. A comma-separated list stacks, which is how a glow is built
Opacityopacitynumber
Groundthe colour tokens, for the subtreePage · Chrome — Chrome points Surface, OnSurface, TextMuted, Muted, Border … at the theme's chrome palette (Chrome, OnChrome, ChromeFaint, ChromeHover, ChromeLine …) for everything inside: how a rail goes dark beside a light page. See [[ui-theming#chrome-palette]]
CaretColorcaret-colorcolor token — the text cursor in an input
AccentColoraccent-colorcolor token — a native checkbox/radio/range, themed without rebuilding it
BgImagebackground-imageCSS value — a gradient or url(…). A theme token reaches it as "linear-gradient(…, var(--colors-accent), …)": every token is a custom property
BgSizebackground-sizeAuto · Cover · Contain
BgPositionbackground-positionCSS value ("center", "50% 20%")
FilterfilterCSS value ("grayscale(1)", "blur(2px)")
BackdropFilterbackdrop-filterCSS value — blurs what is behind the box (a frosted modal scrim)
StrokeDasharraystroke-dasharraynumber or list ("4 2") in the drawing's own units — the dash pattern of every line in an Icon or Svg inside. Inherits
StrokeDashoffsetstroke-dashoffsetnumber — how far along each line the pattern starts. Animate it to make a line draw itself ([[ui-animation-key#draw]]). Inherits
MixBlendModemix-blend-modeNormal · Multiply · Screen · Overlay · Darken · Lighten · ColorDodge · ColorBurn · HardLight · SoftLight · Difference · Exclusion · Hue · Saturation · Color · Luminosity

Drawing a border, a divider, or a focus ring#

Border (above) sets only the color — a border becomes visible once it has width. BorderW turns a solid, theme-colored hairline on; Border recolors it. Per-edge widths draw a single rule — a rail's right edge, a table row's bottom edge — without a full box.

PropCSSValue
BorderWborder-widthlength (e.g. 1 → 1px)
BorderTWborder-top-widthlength
BorderRWborder-inline-end-widthlength — the END edge (the right in English, the left in Arabic)
BorderBWborder-bottom-widthlength
BorderLWborder-inline-start-widthlength — the START edge
BorderStyleborder-styleSolid · Dashed · Dotted · None

A hairline is BorderW = 1; (the style defaults to solid); recolor it with Border = <token>. A divider under a list row is BorderBW = 1; on the row.

An outline is drawn outside the box and takes no layout space, which is what makes it the right tool for a focus ring: showing one must not move the page.

PropCSSValue
OutlineWoutline-widthlength (2 → 2px)
OutlineColoroutline-colorcolor token
OutlineStyleoutline-styleSolid · Dashed · Dotted · None
OutlineOffsetoutline-offsetlength — the gap between the box and the ring

A custom focus ring is those four inside a Focus { … } block. Only replace the default one, never remove it: Outline* with no visible result is a keyboard user losing their place on the page.

Padding, margin, and the gap between children#

⚑ The whole spacing and sizing vocabulary, stated compactly. Measured on eval run 261: the model wanted a full-viewport page, could not find minH in the style guide, wrote "safer to skip it", and shipped a worse layout for a prop that has always worked.

⚑ Why a FENCE and not a paragraph. osy docs ui-styling prints every code fence in full and replaces the prose with a line saying how much there is — so a prop named only in a sentence is absent from the answer the command actually gives, while a prop named in a fence is always in it. These props were prose, and that is why a run could look them up and not find them.

// SPACING — a number is a step on the 0.25rem scale (`p: 4` is 1rem); a string passes through (`mx: "auto"`).
Box(p: 4, px: 6, py: 2, pt: 1, pr: 1, pb: 1, pl: 1);      // padding
Box(m: 4, mx: "auto", my: 2, mt: 1, mr: 1, mb: 1, ml: 1); // margin
Stack(gap: 3);                                            // flex/grid gap — same scale, or a Space token

// SIZE — a number is px, a string passes through, or a Length token.
Stack(w: "100%", h: "4rem");
Stack(minW: "20rem", minH: Length.Screen);   // Length.Screen is how a page fills the viewport (100dvh)
Stack(maxW: "34rem", maxH: "40rem");   // maxW is how a column stops growing on a wide screen
Stack(w: Length.Measure);              // …or a Length token the kit ships — `osy kit tokens` lists every one
// ⚠ THERE IS NO `Length.Column`. A page-column width is YOURS to name — declare it in your own theme first:
//      theme Default { Length { Column = "820px"; } }      // …then `Stack(maxW: Length.Column)` resolves
//   Without that declaration `Length.Column` is a compile error, not a fallback.
PropCSSValue
P Px Py Pt Pr Pb Plpaddinga number is a step on the 0.25rem scale — p: 4 is 1rem; a string passes through (px: "auto")
M Mx My Mt Mr Mb Mlmarginsame scale; mx: "auto" is how a block centres

Every horizontal side is a START or END side. Pl/Ml/BorderLW/Left name the side a line starts on and Pr/Mr/BorderRW/Right the side it ends on — the left and right of an English page, mirrored in an Arabic one, with nothing in the app written for it (Right to left — an app that reads in Arabic with no change to the app). They lower to CSS's logical properties (padding-inline-start, margin-inline-end, inset-inline-start …); Px/Mx to padding-inline/margin-inline. | Gap | gap | the flex/grid gap, same 0.25rem scale, or a Space token |

How big is it — W/H, min/max, and flex grow#

PropCSSValue
W Hwidth heighta number → px, a string → as-is (w: "100%"), or a Length token
MinW MinHmin-width min-heightsame. minH: Length.Screen is how a page fills the viewport — the kit token, 100dvh under the hood, so a phone's address bar does not clip it
MaxW MaxHmax-width max-heightsame. maxW: "34rem" is how a column stops growing on a wide screen

…and the flex sizing trio:

PropCSSValue
Growflex-grownumber — Grow = 1 makes a child fill the space its siblings leave
Shrinkflex-shrinknumber — Shrink = 1 lets a child shrink below a size you stated (see below)
Basisflex-basislength / size token — a child's starting size before grow and shrink apply
AspectRatioaspect-ratioa ratio ("16 / 9", "1") — reserves the box's shape before an image loads

A size you state is a size you get

A definite size on the main axis holds. W on a child of a Row, or H on a child of a Stack, is not a suggestion that the layout may overrule: a child written H = 2000 is 2000 tall, and a Row whose children total more than its width overflows rather than squeezing them.

Stack(h: 150, overflowY: Overflow.Auto) {
  Box(h: 2000) { Text("tall"); }     // 2000 tall — so the Stack scrolls
}

⚠ THIS IS A DELIBERATE DEVIATION FROM CSS, and the one place in the style vocabulary where a prop does not mean exactly what its CSS twin means. In CSS flex-shrink: 1 is the initial value, so an explicit height is only a starting point and a flex child shrinks past it to fit. That default is right for a language where you write flex-basis and think in flex terms; it is wrong for one where you write h: 2000, because writing a number is the statement that you want that number.

"Definite" means a plain length — 150, 12.5rem, 50vh. It does not include a percentage (which resolves against the container, and is exactly where shrinking is the point), the content-driven keywords (auto, fit-content, min-content, max-content), or anything computed or referenced (calc(…), min(…), clamp(…), a size token) — those could hold any of the above, so they keep the CSS default.

To opt back in, say so: Shrink = 1 makes a child shrink again, and being an inline style it beats the rule, so nothing is unreachable. Reach for it in the case it is meant for — a responsive toolbar whose items may compress. Though the more usual answer there is not to state a width at all, and use Grow or MinW = 0 instead.

⚑ The cross axis is untouched. H on a child of a Row never shrank, and still does not — flex-shrink only governs the main axis.

Styling text — font, weight, alignment, truncation#

PropCSSValue
FontSizefont-sizelength / size token
FontWeightfont-weightnumber (600)
FontStylefont-styleNormal · Italic
LineHeightline-heightnumber — unitless (1.4), so it scales with the font size
LetterSpacingletter-spacinglength (1 → 1px) or a string ("0.06em")
FontFamilyfont-familya string ("Inter, system-ui, sans-serif") or a token
TextAligntext-alignLeft · Center · Right · Justify · Start · End — Left/Right are the start and end of the line, so a right-aligned price column sits at the end in Arabic too
FlowDirectiondirectionLeftToRight · RightToLeft — which way this element and everything inside it flows, whatever the page's language: the escape for geometry that must not mirror (a chart's plot, a timeline, a phone number). See Right to left — an app that reads in Arabic with no change to the app
TextTransformtext-transformNone · Uppercase · Lowercase · Capitalize
WhiteSpacewhite-spaceNormal · Nowrap · Pre · PreWrap · PreLine
FontVariantfont-variant-numericNormal · TabularNums · SlashedZero · OldstyleNums
TextDecorationtext-decorationNone · Underline · LineThrough · Overline
TextOverflowtext-overflowClip · Ellipsis
WordBreakword-breakNormal · BreakAll · KeepAll · BreakWord
VerticalAlignvertical-alignBaseline · Top · Middle · Bottom · Sub · Super
TextWraptext-wrapWrap · Nowrap · Balance · Pretty · Stable — Balance evens the lines of a short centred headline so it never ends on one orphaned word; Pretty avoids a lonely last word in a paragraph
TextUnderlineOffsettext-underline-offsetlength (4 → 4px) — how far below the baseline an underline sits; a typeset link, not the browser's default
FontVariationSettingsfont-variation-settingsCSS value ("'wght' 650, 'wdth' 80") — a variable font's axes, set anywhere along them; animatable, so a specimen's weight can breathe
FontFeatureSettingsfont-feature-settingsCSS value ("'ss01', 'case'") — OpenType features: a stylistic set, an alternate glyph, case-sensitive punctuation
TextStroke-webkit-text-strokeCSS value ("2px #000") — an outline on the glyphs; with color: Colors.Transparent the letters are hollow

WhiteSpace = WhiteSpace.Nowrap keeps a tab, a table cell, or a button on one line (the row's own OverflowX handles the excess). A tracked-out uppercase section label is TextTransform = TextTransform.Uppercase; LetterSpacing = "0.06em";; a numeric table column right-aligns with TextAlign = TextAlign.Right;. FontVariant = FontVariant.TabularNums gives every digit the same width so a column of numbers — a metric, a price, a count — lines up vertically instead of jittering as the digits change.

A truncated line is three props, not one: TextOverflow = TextOverflow.Ellipsis needs WhiteSpace = WhiteSpace.Nowrap and Overflow = Overflow.Hidden beside it, or there is nothing to truncate. WordBreak = WordBreak.BreakWord is the other answer — for a long URL or an unspaced identifier that would otherwise widen its container and push the whole layout sideways.

Pinning, layering, scrolling and transforms#

These are what let an app build a drawer, a sticky header, or a modal out of ordinary style props — the platform widens this vocabulary rather than shipping the component.

propCSSvalues
PositionpositionStatic · Relative · Absolute · Fixed · Sticky
ContainerTypecontainer-typeNormal · Size · InlineSize — makes the element a container its descendants size against: inside an InlineSize one, fontSize: "21cqi" is 21% of ITS width, so a wordmark fills its column exactly
DisplaydisplayNone · Block · Flex · Grid · InlineFlex · Contents
OverflowoverflowVisible · Hidden · Auto · Scroll
OverflowXoverflow-xas Overflow — a table scrolls horizontally inside its card while the page owns the vertical scroll
OverflowYoverflow-yas Overflow
ScrollbarWidthscrollbar-widthAuto · Thin · None — so a tab strip scrolls without a chunky gutter
EdgeFademask-imageNone · Start · End · Both · Top · Bottom — the content fades out over that edge, so a row that scrolls sideways (overflowX: Overflow.Auto, edgeFade: EdgeFade.End) shows there is more past it instead of cutting an item off mid-word. Start/End turn with a right-to-left page
ScrollSnapTypescroll-snap-typeX · Y · Both — always mandatory: a swiped gallery comes to rest on a photograph, never between two. Snapping acts only on an element that scrolls, so a breakpoint turns it off by turning the overflow off
ScrollSnapAlignscroll-snap-alignStart · Center · End — where each child of a snapping scroller comes to rest
ScrollPaddingX / ScrollPaddingYscroll-padding-inline / -top+-bottomspacing, as Px/Py — where "start" is. A snapping row with a gutter (px: 8) snaps its first child flush to the EDGE unless it also says scrollPaddingX: 8
ScrollMarginTopscroll-margin-topspacing — the room a SECTION keeps above itself when it is scrolled to (Link(to:), Navigation.ScrollTo, scrollIntoView:): a sticky header's height, so the header does not cover its first line. See [[ui-sections#sticky-header]]
ScrollBehaviorscroll-behaviorAuto · Smooth — whether a programmatic scroll, from a bound scrollTop or a scrollIntoView, jumps or glides. Link(to:) and Navigation.ScrollTo glide anyway, and jump under reduced motion
Insetinsetlength — all four offsets at once
Top / Right / Bottom / Lefttop / inset-inline-end / bottom / inset-inline-startlength — Left/Right are the start and end, so a badge pinned right: 0 sits at the end corner in Arabic too
Zz-indexa ZIndex token
Transitiontransitiona motion token

Motion is four props with a page of its own — animation — keyframe motion, on entry, on exit, in a loop, or as it scrolls into view is where the

propCSSvalues
Animationanimationnames a declared animation block
AnimationDelayanimation-delayduration — offsets one element's copy of it, which is how N elements running one animation become a chase rather than a lockstep
ExitAnimation(played by the renderer)names a declared animation that plays as the element leaves — see [[ui-animation#exit]]
AnimationTimelineanimation-timelineAuto · View — View plays the animation as the element scrolls into view
FieldSizingfield-sizingFixed · Content — an input that grows with what is typed into it

All four are ordinary style props — usable inline or in a variants block — and animation — keyframe motion, on entry, on exit, in a loop, or as it scrolls into view is where the declaration, the timing vocabulary and the staggering example live.

Transforms move, turn and resize a box without re-running layout, so they composite on the GPU and cost nothing per frame. That is why a drawer slides with TranslateX rather than by animating Left.

PropCSSValue
TranslateXtransform: translateX(…)length or a string ("-100%") — towards the END: it mirrors in a right-to-left page, so a drawer slides in from its own edge
TranslateYtransform: translateY(…)length or a string
Rotaterotatean angle ("45deg")
Scalescalea number (1.05) or a pair ("1.1 1")
TransformOrigintransform-originthe point it turns/scales about ("center", "top left")
Mirrortransform: scaleX(…)Never · InRightToLeft — a photograph composed for one side (words set in its empty half) turns with the page

TranslateX and TranslateY both write the single transform property, so the compiler merges them into one declaration — setting both is transform: translateX(a) translateY(b), not one silently overwriting the other. Rotate/Scale are CSS's own individual properties and compose on their own.

Cursor, click-through, selection, hiding#

What a box does to the pointer, the caret and the selection — the props that make a decorative overlay click-through, or a label un-selectable so a double-click selects the row instead of the word.

PropCSSValue
CursorcursorAuto · Default · Pointer · Text · Move · NotAllowed · Grab · Grabbing · Wait · Help · Crosshair · ColResize · RowResize
PointerEventspointer-eventsAuto · None — None makes a box invisible to the mouse; clicks pass through to whatever is beneath
UserSelectuser-selectAuto · None · Text · All
ResizeresizeNone · Both · Horizontal · Vertical — a user-draggable textarea
VisibilityvisibilityVisible · Hidden · Collapse — hidden but still occupying its space (unlike Display = Display.None)
ObjectFitobject-fitFill · Contain · Cover · None · ScaleDown — how an image fills its box
ObjectPositionobject-positionCSS value ("center 30%", "top") — which part of a cropped image stays in frame; Cover alone keeps the centre
AlignSelfalign-selfAuto · Start · Center · End · Stretch · Baseline — one child opting out of the row's alignment
Orderordernumber — reorders a flex/grid child visually without moving it in the DOM

⚠ Order changes only the PAINTED order. Tab order and screen-reader order still follow the source, so a visual order that disagrees with the document order is an accessibility bug, not a layout trick.

Lining columns up across rows — grid#

With Display = Display.Grid, these define a grid — the way to align columns across rows (a data table) without a shipped Table component. Children flow into the tracks in order.

PropCSSValue
Colsgrid-template-columnsa number of equal columns (3 → repeat(3, minmax(0, 1fr))), or a track list ("2fr 1fr 1fr", "repeat(4, 1fr)", "auto 1fr auto"). Only a grid reads it: an atom given cols: or rows: without display: Display.Grid is a compile error, since it would lay out nothing
Rowsgrid-template-rowsa track list
ColSpangrid-columnhow many columns a cell straddles ("span 2", "1 / -1")
RowSpangrid-rowhow many rows a cell straddles
GridAreagrid-areaa named area or an explicit span ("1 / 1 / 3 / 2")
GridAutoFlowgrid-auto-flowRow · Column — which way items that outrun the declared tracks flow
GridAutoRowsgrid-auto-rowsthe size of a row the track list did not declare ("minmax(40px, auto)")
GridAutoColsgrid-auto-columnsthe same, for columns
Wrapflex-wrapWrapping.Wrap · Wrapping.Nowrap · Wrapping.WrapReverse — flex, not grid: whether a row breaks onto a second line. A bool reads as the flag it is: true is Wrap and false is Nowrap, literal or computed (wrap: !wide, a bool parameter or state field), chosen at runtime. The vocabulary is Wrapping, so the prop and the value do not stutter

A table header + rows all using the same Cols line up automatically; Gap (see spacing) sets the grid gap. The GridAuto* props take over when the data outruns the declared tracks, which is the usual case for a list of unknown length: declare Cols and let the rows generate themselves.

Styling hover, focus, disabled, and one screen width#

A nested Hover { … } / Focus { … } / Active { … } / Disabled { … } block styles that interaction state; a nested breakpoint block (Tablet { … } — one of the kit's Base/Phone/Tablet/Laptop/Desktop/Widescreen, or a name your theme's Breakpoints declares) applies its props only at that width and up (mobile-first). Both nest inside any variant value.

Styling one atom without minting a component#

The style props above are usually set in a component's variants recipe, but an atom can also carry them inline as call arguments — the C#-natural way to style a one-off without minting a component for it:

Text("osyrin", fontSize: 17, fontWeight: FontWeight.Medium, color: Colors.TextPrimary)
Box(bg: Colors.Surface, p: 4, rounded: Radius.Card)
Icon(Icons.Chev, size: 18, color: Colors.TextMuted)

Argument names are the camelCase of the prop (fontSize, bg, p, rounded); values are the same forms as in a variant (a theme token, a number, a string, a keyword) — or a conditional that picks between them (below). A hover style still belongs in variants (it is a pseudo-state, not a value choice). A responsive value can be either: a breakpoint block in variants, or a conditional on the live width right at the call — cols: Layout.AtLeast(Tablet) ? "2fr 1fr 1fr" : "1fr" (layout primitives) — which is the short form for one value.

⚠ Inline, a name value is written QUALIFIED — bg: Colors.Surface, not bg: Surface. An argument slot accepts every kind of value, so a bare capitalised name there could be a theme token, an enum member or a style keyword, and all three are spelled alike; the group name is what says which vocabulary you meant. Each prop's group is the one its values come from — Colors for bg/color/border, Radius for rounded, Shadow for shadow, FontSize / FontWeight / Font for the type props, and the keyword props take their own name (Display.Flex, Position.Fixed, Overflow.Auto, Cursor.Pointer). A bare name is refused, and the refusal names the exact spelling to write.

⚑ A variants block follows the same rule — Bg = Colors.Surface;, Rounded = Radius.Card;, Display = Display.Flex;. There is no position where a value is written bare. The prop on the left does not decide the vocabulary, which is the thing that makes the rule uniform rather than arbitrary: any group's token is accepted for any prop, so Bg = Radius.Card; is a legal (if odd) thing to write and the group is genuinely carrying information.

The one sentence is: a value names the vocabulary it comes from. Not "when it is ambiguous" — always, so you never have to work out which case you are in.

An inline value may be a conditional (cond ? A : B) whose branches are each an ordinary style value — so one property can depend on one piece of component state without minting an enum + a variants dimension for it:

theme App {
  Colors { Surface2 = "#eef1f5"; TextPrimary = "#111827"; TextMuted = "#6b7280"; }
  Radius { Control = "8px"; }
}

[Composable] component Tab(string label, bool active, Action onClick) {
  render {
    Row(bg: active ? Colors.Surface2 : "transparent", color: active ? Colors.TextPrimary : Colors.TextMuted, rounded: Radius.Control) {
      Text(label);
    }
  }
}

The condition is a Boolean read from component state; the value re-evaluates and the element re-renders whenever that state changes. Branches may mix a token and a string and may nest (bg: a ? (b ? "#fee" : Colors.Border) : Colors.Bg). For a whole set of properties that changes together across several states, an enum-typed state variant is still the better fit — a conditional value is for the common "one property, one condition" case.

Giving an element a NAME — not a style prop#

A style prop says how an element looks. Three other ambient vocabularies say what it is, what state it is in, and what it is called — and the last one is the one people miss, because the obvious-looking candidate is a style prop's neighbour in the same argument list:

Input(value: email, placeholder: "[email protected]")            // ✗ has NO name
Text("Email", labelFor: field); Input(value: email, id: field) // ✓ named, and the words are clickable

A placeholder: is not a name: it disappears the moment you type, is not announced as the field's name, and nothing can address the field by it. The naming props (labelFor:/id:, labelledBy:, describedBy:, label:) are ambient — legal on every element, so they appear in no atom's signature. osy kit atoms lists them under Ambient; accessibility is the full reference.

Styling a child when an ANCESTOR is hovered — inside#

A nested inside <Component>.<State> { … } block styles this element when it sits inside an ancestor component that is in a given interaction state — the reveal-on-hover pattern (a row's actions, a card's menu, a tab's close):

theme App {
  Motion { Fade = "opacity 0.12s ease"; }
}

[Composable] component RowActions() {
  variants {
    base {
      Opacity = 0; Transition = Motion.Fade;          // hidden by default…
      inside DataRow.Hover { Opacity = 1; }    // …shown when its DataRow is hovered
    }
  }
  render { Row { Text("⋯"); } }
}

[Composable] component DataRow() {
  render { Row { Slot; } }
}

The child names the ancestor, so it stays reusable in any matching container and the container needs to know nothing about it. <State> is one of the universal interaction states (Hover / Active / Focus) — a declared state like "selected" is data the child can see, so it rides an ordinary state variant fed by a prop, not inside. The block nests inside a value block (usually base), like a pseudo-state.

Examples#

A flat, hairline-bordered card with a hover lift — the whole look from the vocabulary, no shipped component:

theme App {
  Colors { Surface = "#ffffff"; Border = "#e5e7eb"; }
  Radius { Card = "12px"; }
}

[Composable] component Card() {
  variants {
    base {
      Bg = Colors.Surface; BorderW = 1; Border = Colors.Border; Rounded = Radius.Card; P = 4;
      Hover { BorderW = 1; Shadow = "0 1px 3px rgba(0,0,0,0.08)"; }
    }
  }
  render { Stack(gap: 2) { Slot; } }
}

A list row with a bottom divider, the last row suppressing it via a passed-in state:

theme App {
  Colors { Border = "#e5e7eb"; }
}

enum RowEdge { Divided, Last }

[Composable] component ListRow(RowEdge edge) {
  variants {
    base { Px = 3; Py = 2; }
    edge {
      Divided { BorderBW = 1; Border = Colors.Border; }
      Last { }
    }
  }
  render { Row(justify: Justify.Between) { Slot; } }
}

See also#

Related

theme tokens

A `theme` block names your app's design tokens — colors, spacing, radii, and more — as reusable values. A token can…

layout primitives

The built-in layout primitives and how they arrange children. `Stack` stacks children in a column, `Row` lays them in a…

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…

animation — keyframe motion, on entry, on exit, in a loop, or as it scrolls into view

An `animation` block declares reusable keyframe motion — an entrance, a shimmer, a pulse, a flash that decays. Its…

accessibility

Tags already give an element its role, focus and keyboard behaviour. The semantic props say the rest: `role:` for a…

Right to left — an app that reads in Arabic with no change to the app

A language written right to left (Arabic, Hebrew, Persian, Urdu) mirrors the whole page: every horizontal side a style…