# style props

> 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.

<!-- id: ui-styling · area: ui · stability: stable · html: https://osysharp.com/reference/ui/styling/ -->

## Summary        {#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.

```osy syntax
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](https://osysharp.com/reference/ui/theming/) 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](https://osysharp.com/reference/ui/layout/).

## Signature      {#signature}
```osy syntax
variants {
  base {
    <StyleProp> = <number | keyword | tokenName>;
    ...
  }
}
```

## Description    {#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   {#which-element}
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:

```osy title="both arms are roots, so both are styled" syntax
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.

### What color is it — background, text, shadow, opacity   {#paint}

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.

| Prop | CSS | Value |
|---|---|---|
| `Bg` | `background-color` | color token |
| `Color` | `color` | color token |
| `Border` | `border-color` | color token |
| `Rounded` | `border-radius` | length / radius token |
| `Shadow` | `box-shadow` | CSS value, or a shadow token — haloes the element's **rectangle** |
| `TextShadow` | `text-shadow` | CSS value — a shadow on the **glyphs**. A comma-separated list stacks, which is how a glow is built |
| `Opacity` | `opacity` | number |
| `CaretColor` | `caret-color` | color token — the text cursor in an input |
| `AccentColor` | `accent-color` | color token — a native checkbox/radio/range, themed without rebuilding it |
| `BgImage` | `background-image` | CSS value — a gradient or `url(…)`. A theme token reaches it as `"linear-gradient(…, var(--colors-accent), …)"`: every token *is* a custom property |
| `BgSize` | `background-size` | `Auto` · `Cover` · `Contain` |
| `BgPosition` | `background-position` | CSS value (`"center"`, `"50% 20%"`) |
| `Filter` | `filter` | CSS value (`"grayscale(1)"`, `"blur(2px)"`) |
| `BackdropFilter` | `backdrop-filter` | CSS value — blurs what is *behind* the box (a frosted modal scrim) |
| `MixBlendMode` | `mix-blend-mode` | `Normal` · `Multiply` · `Screen` · `Overlay` · `Darken` · `Lighten` · `ColorDodge` · `ColorBurn` · `HardLight` · `SoftLight` · `Difference` · `Exclusion` · `Hue` · `Saturation` · `Color` · `Luminosity` |

### Drawing a border, a divider, or a focus ring   {#borders}
`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.

| Prop | CSS | Value |
|---|---|---|
| `BorderW` | `border-width` | length (e.g. `1` → `1px`) |
| `BorderTW` | `border-top-width` | length |
| `BorderRW` | `border-right-width` | length |
| `BorderBW` | `border-bottom-width` | length |
| `BorderLW` | `border-left-width` | length |
| `BorderStyle` | `border-style` | `Solid` · `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.

| Prop | CSS | Value |
|---|---|---|
| `OutlineW` | `outline-width` | length (`2` → `2px`) |
| `OutlineColor` | `outline-color` | color token |
| `OutlineStyle` | `outline-style` | `Solid` · `Dashed` · `Dotted` · `None` |
| `OutlineOffset` | `outline-offset` | length — 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   {#spacing}

⚑ **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.

```osy syntax
// 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.
```

| Prop | CSS | Value |
|---|---|---|
| `P` `Px` `Py` `Pt` `Pr` `Pb` `Pl` | `padding` | a **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` `Ml` | `margin` | same scale; `mx: "auto"` is how a block centres |
| `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   {#size}

| Prop | CSS | Value |
|---|---|---|
| `W` `H` | `width` `height` | a **number** → `px`, a **string** → as-is (`w: "100%"`), or a `Length` token |
| `MinW` `MinH` | `min-width` `min-height` | same. `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` `MaxH` | `max-width` `max-height` | same. `maxW: "34rem"` is how a column stops growing on a wide screen |

…and the flex sizing trio:

| Prop | CSS | Value |
|---|---|---|
| `Grow` | `flex-grow` | number — `Grow = 1` makes a child fill the space its siblings leave |
| `Shrink` | `flex-shrink` | number — `Shrink = 1` lets a child shrink below a size you stated (see below) |
| `Basis` | `flex-basis` | length / size token — a child's starting size before grow and shrink apply |
| `AspectRatio` | `aspect-ratio` | a ratio (`"16 / 9"`, `"1"`) — reserves the box's shape before an image loads |

#### A size you state is a size you get   {#definite-size-holds}

**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.

```osy syntax
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   {#type}
| Prop | CSS | Value |
|---|---|---|
| `FontSize` | `font-size` | length / size token |
| `FontWeight` | `font-weight` | number (`600`) |
| `FontStyle` | `font-style` | `Normal` · `Italic` |
| `LineHeight` | `line-height` | number — **unitless** (`1.4`), so it scales with the font size |
| `LetterSpacing` | `letter-spacing` | length (`1` → `1px`) or a string (`"0.06em"`) |
| `FontFamily` | `font-family` | a string (`"Inter, system-ui, sans-serif"`) or a token |
| `TextAlign` | `text-align` | `Left` · `Center` · `Right` · `Justify` · `Start` · `End` |
| `TextTransform` | `text-transform` | `None` · `Uppercase` · `Lowercase` · `Capitalize` |
| `WhiteSpace` | `white-space` | `Normal` · `Nowrap` · `Pre` · `PreWrap` · `PreLine` |
| `FontVariant` | `font-variant-numeric` | `Normal` · `TabularNums` · `SlashedZero` · `OldstyleNums` |
| `TextDecoration` | `text-decoration` | `None` · `Underline` · `LineThrough` · `Overline` |
| `TextOverflow` | `text-overflow` | `Clip` · `Ellipsis` |
| `WordBreak` | `word-break` | `Normal` · `BreakAll` · `KeepAll` · `BreakWord` |
| `VerticalAlign` | `vertical-align` | `Baseline` · `Top` · `Middle` · `Bottom` · `Sub` · `Super` |

`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   {#placement}
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.

| prop | CSS | values |
|---|---|---|
| `Position` | `position` | `Static` · `Relative` · `Absolute` · `Fixed` · `Sticky` |
| `Display` | `display` | `None` · `Block` · `Flex` · `Grid` · `InlineFlex` · `Contents` |
| `Overflow` | `overflow` | `Visible` · `Hidden` · `Auto` · `Scroll` |
| `OverflowX` | `overflow-x` | as `Overflow` — a table scrolls horizontally inside its card while the page owns the vertical scroll |
| `OverflowY` | `overflow-y` | as `Overflow` |
| `ScrollbarWidth` | `scrollbar-width` | `Auto` · `Thin` · `None` — so a tab strip scrolls without a chunky gutter |
| `ScrollBehavior` | `scroll-behavior` | `Auto` · `Smooth` — whether a programmatic scroll, from a bound `scrollTop` or a `scrollIntoView`, jumps or glides |
| `Inset` | `inset` | length — all four offsets at once |
| `Top` / `Right` / `Bottom` / `Left` | `top` / `right` / `bottom` / `left` | length |
| `Z` | `z-index` | a `ZIndex` token |
| `Transition` | `transition` | a motion token |

**Motion** is two props with a page of its own — [animation — looping motion with no destination state](https://osysharp.com/reference/ui/animation/) is where the

| prop | CSS | values |
|---|---|---|
| `Animation` | `animation` | names a declared `animation` block |
| `AnimationDelay` | `animation-delay` | duration — offsets one element's copy of it, which is how N elements running one animation become a chase rather than a lockstep |
| `FieldSizing` | `field-sizing` | `Fixed` · `Content` — an input that grows with what is typed into it |

Both are ordinary style props — usable inline or in a `variants` block — and [animation — looping motion with no destination state](https://osysharp.com/reference/ui/animation/) 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`.

| Prop | CSS | Value |
|---|---|---|
| `TranslateX` | `transform: translateX(…)` | length or a string (`"-100%"`) |
| `TranslateY` | `transform: translateY(…)` | length or a string |
| `Rotate` | `rotate` | an angle (`"45deg"`) |
| `Scale` | `scale` | a number (`1.05`) or a pair (`"1.1 1"`) |
| `TransformOrigin` | `transform-origin` | the point it turns/scales about (`"center"`, `"top left"`) |

`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   {#interaction}
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.

| Prop | CSS | Value |
|---|---|---|
| `Cursor` | `cursor` | `Auto` · `Default` · `Pointer` · `Text` · `Move` · `NotAllowed` · `Grab` · `Grabbing` · `Wait` · `Help` · `Crosshair` · `ColResize` · `RowResize` |
| `PointerEvents` | `pointer-events` | `Auto` · `None` — `None` makes a box invisible to the mouse; clicks pass through to whatever is beneath |
| `UserSelect` | `user-select` | `Auto` · `None` · `Text` · `All` |
| `Resize` | `resize` | `None` · `Both` · `Horizontal` · `Vertical` — a user-draggable textarea |
| `Visibility` | `visibility` | `Visible` · `Hidden` · `Collapse` — hidden but still occupying its space (unlike `Display = Display.None`) |
| `ObjectFit` | `object-fit` | `Fill` · `Contain` · `Cover` · `None` · `ScaleDown` — how an image fills its box |
| `AlignSelf` | `align-self` | `Auto` · `Start` · `Center` · `End` · `Stretch` · `Baseline` — one child opting out of the row's alignment |
| `Order` | `order` | number — 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   {#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.

| Prop | CSS | Value |
|---|---|---|
| `Cols` | `grid-template-columns` | a track list (`"2fr 1fr 1fr"`, `"repeat(4, 1fr)"`, `"auto 1fr auto"`) |
| `Rows` | `grid-template-rows` | a track list |
| `ColSpan` | `grid-column` | how many columns a cell straddles (`"span 2"`, `"1 / -1"`) |
| `RowSpan` | `grid-row` | how many rows a cell straddles |
| `GridArea` | `grid-area` | a named area or an explicit span (`"1 / 1 / 3 / 2"`) |
| `GridAutoFlow` | `grid-auto-flow` | `Row` · `Column` — which way items that outrun the declared tracks flow |
| `GridAutoRows` | `grid-auto-rows` | the size of a row the track list did not declare (`"minmax(40px, auto)"`) |
| `GridAutoCols` | `grid-auto-columns` | the same, for columns |
| `Wrap` | `flex-wrap` | `Wrapping.Wrap` · `Wrapping.Nowrap` · `Wrapping.WrapReverse` — flex, not grid: whether a row breaks onto a second line. 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](#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   {#states}
A nested `Hover { … }` / `Focus { … }` / `Active { … }` / `Disabled { … }` block styles that interaction state; a
nested breakpoint block (`Cozy { … }`) applies its props only at that width and up (mobile-first). Both nest inside
any variant value.

### Styling one atom without minting a component   {#inline}
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:

```osy title="style a one-off atom without minting a component" syntax
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* or *responsive* style still belongs in `variants` (those are pseudo-states and breakpoints, not value choices).

⚠ **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:

```osy title="inline style props, chosen per render" test app=ui-styling-inline
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   {#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:

```osy syntax
Input(value: email, placeholder: "you@example.com")            // ✗ 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](https://osysharp.com/reference/ui/accessibility/) is the full reference.

### Styling a child when an ANCESTOR is hovered — `inside`   {#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):

```osy title="styling driven by an ANCESTOR's state" test app=ui-styling-inside
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       {#examples}

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

```osy title="a variant recipe" test app=ui-styling-recipe
theme App {
  Colors { Surface = "#ffffff"; Border = "#e5e7eb"; }
  Radius { Card = 12; }
}

[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:

```osy title="an enum dimension" test app=ui-styling-enum
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       {#see-also}
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the design tokens a style prop's value references by name
- [layout primitives](https://osysharp.com/reference/ui/layout/) — `Stack`/`Row`/`Box` and the `align`/`justify`/`gap` layout arguments
- [component](https://osysharp.com/reference/ui/component/) — where a `variants` block lives and how a component is authored
- [accessibility](https://osysharp.com/reference/ui/accessibility/) — the other ambient argument vocabularies: what an element IS, what STATE it is in, and what it is CALLED
