# theme tokens

> A `theme` block names your app's design tokens — colors, spacing, radii, and more — as reusable values. A token can hold a literal value (`Primary = "#0077B6"`) or reference another token by name (`Grid = Border`), so shared values stay defined in one place.

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

## Summary        {#summary}
A **`theme`** block declares your app's **design tokens** — the named values (colors, spacing steps, radii, …)
your UI is built from. Grouping them in a theme means a value like your brand color is defined **once** and reused
everywhere, so restyling the app is a change in one place.

`osy kit --tokens` prints the tokens you already have — the starter theme the [Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/) ships, by group and with
each value — which is where to look before inventing a name.

```osy title="tokens, grouped" test app=ui-theming
theme Default {
  Colors {
    Primary = "#0077B6";
    OnPrimary = "#FFFFFF";
    Border = "#E3E6EA";
  }
  Radius {
    Md = 10;
    Lg = 16;
  }
}
```

Tokens are organized into **groups**. Each leaf assignment (`Primary = "#0077B6"`) is one token; its **path**
through the groups (`Colors` → `Primary`) is what makes its name unique, so two groups can both have a `Md` without
colliding.

The top-level groups are a **closed set** — the token *kinds* the editor understands, so `Bg = ‹caret›` can offer
your colors, `FontSize = ‹caret›` your sizes, and so on:

> `Colors` · `Space` · `Radius` · `Font` · `FontSize` · `FontWeight` · `Shadow` · `Motion` · `ZIndex` ·
> `Breakpoints` · `Series` · `Length` · `Density` · `Touch`

The font kinds are **separate flat groups** (`Font` for families, `FontSize`, `FontWeight`) rather than one mixed
`Type { … }`, so each token's kind is unambiguous. An unrecognized group name is a **compile error** with a
did-you-mean — a theme group that isn't one of these is a typo, not a silent no-op.

## Signature      {#signature}
```osy syntax
theme { Colors { … } Radius { … } } — named design tokens, and references between them
```

## Description    {#description}

### Where a theme goes — and why there is nothing to wire   {#where}
**Declaring it is the whole step.** Put the `theme` block in any file the manifest's `model` glob already covers —
`model/theme.osy` by convention — and every control and page in the app is styled by it. There is no registration
call, no `Theme =` on the app, nothing to import: a theme is part of the model, like an entity.

The **name is only a name.** `theme Default` and `theme Anything` behave identically; light and dark are a property
of each TOKEN (`Modes.Of(light: …, dark: …)` — see [[ui-theming#modes]]), not of the block.

*(Said here because its absence is what a reader goes looking for. Every other framework has a provider, a plugin or
a config key, so "I have written the tokens — now how do I attach them?" is the next question, and a page that never
answers it reads as incomplete. One run spent a call grepping this page for `app.osy`, `Theme =`, `UseTheme` and
`attach`, and found nothing, because there is nothing.)*

### The groups a theme may declare   {#groups}
A theme's top-level groups are a **closed set** — anything else is a compile error with a did-you-mean. Each group
feeds a family of style props, which is why the grouping exists rather than one flat list of names.

| Group | Holds | Feeds |
|---|---|---|
| `Colors` | colours and palettes | `bg:`, `color:`, `border:` |
| `Space` | the few spacings that are a decision, not a scale step | `p:`, `m:`, `gap:` |
| `Radius` | corner radii — owns the `Sm`/`Md`/`Lg` triple | `rounded:` |
| `Font` | font stacks | `font:` |
| `FontSize` | type sizes, named for the ROLE the text plays | `fontSize:` |
| `FontWeight` | type weights | `fontWeight:` |
| `Shadow` | elevation, named for the elevation not the control | `shadow:` |
| `Motion` | whole transitions, not bare durations | `transition:` |
| `ZIndex` | layering policy | `z:` |
| `Breakpoints` | your own width names — `Layout.AtLeast(Tablet)` reads these | responsive props |
| `Length` | widths, heights, control and row heights | `w:`, `h:`, `minW:`, `maxW:`, `basis:` |
| `Series` | chart series colours | plotting controls |

`Length` is the sizing group. `Size`, `Density` and `Touch` are accepted as named sub-scales of the same kind, so an
existing theme keeps compiling — but the kit spends `Size` on the control-size **enum** (`size: Size.Lg`), and one
spelling standing for two different things is the reason `Length` is what every kit example uses.

### Giving a token a plain value — a color or a number   {#literals}
A token most often holds a **literal** value — a color string or a number:

```osy title="literal tokens" test app=ui-theming
theme Default {
  Colors   { Primary = "#0077B6"; }
  Radius   { Md = 10; }
  FontSize { Body = "13px"; Heading = "22px"; }
}
```

Each literal token becomes a reusable style value your components draw from. Changing the literal changes every
place that uses the token.

### Can one token reuse another's value?   {#references}
A token can reference **another token by name** instead of repeating a value:

```osy title="one token referencing another" test app=ui-theming
theme Default {
  Colors {
    Border = "#E3E6EA";
    Grid   = Colors.Border;      // Grid resolves to whatever Border is
  }
}
```

`Grid = Border` keeps `Grid` pointing at `Border` as a **living reference**, not a copy — if you later change
`Border`, `Grid` follows automatically, with no need to update it or rebuild. A reference names a token by its
plain leaf name; a token in the **same group** is preferred, so the `Border` above binds to the `Border` in
`Colors`.

Referencing a token that doesn't exist is a **compile error** — a typo like `Grid = Bordr` is caught, not
silently ignored (the same way a mistyped color step like `Primary.Hund` is).

### One seed color, a whole ramp — `Palette.From`   {#palettes}
A color token can be a whole **palette** instead of a single value — `Primary = Palette.From("#0077B6")` generates an
even ramp of shades from one seed, and `Primary.Hover` / `Primary[600]` reach its steps. See [color palettes](https://osysharp.com/reference/ui/palette/) for the
full story.

```osy title="a token that is a whole ramp" test app=ui-theming
theme Brand {
  Colors {
    Primary = Palette.From("#0077B6");
    Line    = Primary[200];        // a light step from the ramp
  }
}
```

### Why is my token name a collision? — one leaf, one value   {#unique-names}
A reference names a token by its **group and name** (`Rounded = Radius.Card`) — a bare leaf does not say which
vocabulary it means, since a theme token, an enum member and a style keyword are all spelled alike in that
position. The leaf still has to denote exactly **one** value across your whole app, because the token map is keyed
by it: declaring the same name under two different groups is a compile error:

```osy title="✗ one leaf name under two groups is a collision" syntax
theme Admin {
  Space  { Md = "12px"; }
  Radius { Md = "8px"; }     // error: duplicate token 'Md' declared in groups 'Space' and 'Radius'
}
```

Name tokens by their **role** and the question doesn't arise — `Radius { Control; Card; Pill; }` reads better at
the call site (`Rounded = Card`) than a second `Sm/Md/Lg` scale would, and it can only mean one thing.

Re-declaring a name **under the same group** is not a collision — that is how you override a token a UI kit
shipped, and both resolve to the same value slot.

**Your value wins.** A token you declare **shadows** a same-named one from the kit, exactly as your own component
shadows a kit component of the same name. So a theme of your own needs to restate only what you are changing:

⚑ That holds across GROUPS too, and it is why declaring `FontWeight { Normal = …; }` is not an error even though
the kit ships a `Motion.Normal`: leaf names are one flat namespace, so your declaration takes the leaf and the
kit's becomes unreachable in your app. A use site that names the kit's group is told so —
*"'Normal' is a token in the `FontWeight` group, not `Motion`"* — rather than the whole theme being refused.

```osy title="✓ restate only the kit token you are changing" syntax
theme Brand {
  Colors { Primary = "#0077B6"; }   // shadows the kit's Primary
}
// every other kit token — Surface, Border, Danger — still applies
```

Shadowing is per **token**, not per theme: the kit's other tokens keep applying, so you never have to copy a kit's
whole palette to change one colour of it.

### Does a length token need `px`? — units   {#units}
A token that holds a **length** carries its own unit, as a string:

```osy title="a length carries its unit" test app=ui-theming-units
theme Admin {
  Radius { Card = "12px"; }      // ✓  border-radius: 12px
  Space  { Md = "12px"; }        // ✓  gap: 12px
}
```

A bare number is emitted **unitless**, which is correct for `ZIndex`, `FontWeight` and `Breakpoints` — and wrong
for anything the browser needs a unit for. Style props are the other way round: there a bare number takes the
prop's unit (`Px = 4` → `1rem`), because the prop already knows what it is.

### Dark mode — per-mode values   {#modes}
A token can hold **different values per mode** — most commonly light and dark — by giving it a mode map:

```osy title="one token, two modes" test app=ui-theming
theme Default {
  Colors {
    Surface = Modes.Of(light: "#FFFFFF", dark: "#111111");
    OnSurface = Modes.Of(light: "#111111", dark: "#F5F5F5");
  }
}
```

The **`Light`** value is the default. **`Dark`** applies automatically when the visitor's device prefers a dark
color scheme — the correct colors are there on the very first paint, with **no flash**. You can also force a mode
explicitly (for a theme toggle) by setting `data-theme="dark"` on the page, which wins over the device preference.
A visitor's explicit choice is **remembered across visits** and applied on the first paint of their next visit —
still with no flash — so a returning user always lands in the mode they picked.

A mode value is a normal token value — a literal (as above) or a reference to another token — so everything from
[the sections above](#references) applies inside a mode map too. Modes are open-ended: `Light` and `Dark` are the
common pair, but you can define others and select them with `data-theme`.

### Making the app dark-only (or light-only) — `Mode`   {#mode}
The device preference is the right default for an app that offers **both** looks. An app that has **one** look says
so, with a `Mode` setting at the top of its theme:

```osy title="this app is dark, full stop" test app=ui-theming-mode
theme Midnight {
  Mode = Dark;
  Colors {
    Surface = Modes.Of(light: "#FFFFFF", dark: "#111111");
    Accent  = "#FF2D95";
  }
}
```

Now every token resolves its **`Dark`** value by default, on any device, on the first paint. `Mode = Light;` does the
same in the other direction — worth writing when your app is deliberately light, because without it a visitor whose
phone prefers dark gets the dark column of every mode map you wrote.

It pins the **default**, not the choice: `data-theme` still wins, so [a toggle](#toggle) keeps working in an app that
declares a `Mode`. Leave `Mode` out to follow the device, which is what every theme does by default.

This matters most for **tokens you did not write**. The UI kit's controls paint
with the kit's own `Surface`/`OnSurface`/`Border` tokens — and those carry mode maps. So an app whose own palette is
dark, but which never says `Mode = Dark;`, gets kit buttons and cards in their **light** colors on a light-mode
browser: a white button on a near-black page, from source that reads perfectly.

### Switching mode — a toggle   {#toggle}
Two verbs switch the mode at runtime and **remember the choice**:

- **`Theme.Toggle()`** — flip between light and dark.
- **`Theme.Set(mode)`** — apply a named mode, e.g. `Theme.Set("dark")` (use this when your theme has more than the
  light/dark pair).

Both persist the choice, so it survives navigation and the visitor's next visit (applied on the first paint, no
flash). Call them from an action:

```osy title="your own toggle" test app=ui-theming-toggle
component ModeButton() {
  action Flip() { Theme.Toggle(); }
  render { Pressable(onClick: Flip) { Text("🌓"); } }
}
```

You don't have to write your own — the UI kit ships a ready one. Drop **`ThemeToggle()`**
into any page (pass `label` to change its face, or declare your own same-named `ThemeToggle` to fully restyle it):

```osy title="the kit's ready-made one" test app=ui-theming-toggle
[Page("/")]
[AllowAnonymous]
component Home() {
  render {
    Row { Text("My app"); ThemeToggle(); }
  }
}
```

The platform ships the switching **mechanism** and the kit control, but injects no toggle of its own — where the
button lives is your layout's decision, not the platform's.

### Styling a component — `variants`   {#style-props}
A component styles itself with a `variants` recipe. `base` is what it always looks like; each **dimension** names one
of the component's parameters, and adds the styling that applies for its value.

```osy title="an enum dimension" test app=ui-theming-variants
theme Kit {
  Colors { Surface = "#FFFFFF"; OnSurface = "#111111"; Primary = "#0077B6"; OnPrimary = "#FFFFFF"; Danger = "#C1121F"; }
  Radius { Md = "10px"; }
}

enum Tone { Neutral, Primary, Danger }
enum Size { Sm, Md, Lg }

component Button(Tone tone, Size size) {
  variants {
    base { Bg = Colors.Surface; Color = Colors.OnSurface; Rounded = Radius.Md; Px = 4; Py = 2; }
    tone { Primary { Bg = Colors.Primary; Color = Colors.OnPrimary; } Danger { Bg = Colors.Danger; } }
    size { Sm { Px = 3; Py = 1; } Lg { Px = 5; Py = 3; } }
  }
  render { Text("Save"); }
}
```

An **enum** parameter lists a block per member, as above. A **bool** parameter has only one thing to say, so it says
it directly — these are the styles that apply when it is true:

```osy title="a bool dimension" test app=ui-theming-variants
component Rail(bool collapsed, bool drawerOpen) {
  variants {
    base { W = "264px"; }
    collapsed { W = "64px"; }        // when `collapsed` is true
    drawerOpen { TranslateX = "0"; } // when `drawerOpen` is true
  }
  render { Text("Rail"); }
}
```

There is no `false` block, because not applying the styles is exactly what false means. Writing the enum shape on a
bool (`collapsed { True { … } }`) is a compile error that shows you the spelling above.

Reach for a bool before minting an enum to carry one. A two-member `enum RailMode { Open, Collapsed }` says no more
than `bool collapsed` does, and it costs you at the call site: the component ends up taking the enum *and* a bool for
the same fact, because a variant's enum value is not something the render body can read back as a condition.

The property names are a **closed vocabulary** — a typo is a compile error with a suggestion, not a declaration
that silently does nothing:

| Group | Props |
|---|---|
| Paint | `Bg` `Color` `Border` `Rounded` `Shadow` `Opacity` |
| Spacing | `P` `Px` `Py` `Pt` `Pr` `Pb` `Pl` · `M` `Mx` `My` `Mt` `Mr` `Mb` `Ml` · `Gap` |
| Size | `W` `H` `MinW` `MinH` `MaxW` `MaxH` `Grow` |
| Type | `FontSize` `FontWeight` |
| Placement | `Position` `Display` `Overflow` `Inset` `Top` `Right` `Bottom` `Left` `Z` |
| Motion | `Transition` `TranslateX` `TranslateY` |

**Values are tokens, numbers, or keywords.** A bare name is a [token reference](#references) (`Bg = Surface`) —
which is what lets one theme restyle everything. A number takes the prop's unit: spacing props use the spacing
scale (`Px = 4` → `1rem`), size props are pixels (`W = 280` → `280px`).

`Position`, `Display` and `Overflow` take a **keyword** from a fixed set, because there is nothing a theme could
usefully say about `position: fixed`. A keyword is written **qualified**, with the prop's own name as the group —
`Position = Position.Fixed;` — because a bare `Fixed` could equally be a token you declared, and the two are spelled
alike. (A token reference stays bare, as above: the prop already decides that vocabulary.)

| Prop | Accepts |
|---|---|
| `Position` | `Position.Static` `Position.Relative` `Position.Absolute` `Position.Fixed` `Position.Sticky` |
| `Display` | `Display.None` `Display.Block` `Display.Flex` `Display.Grid` `Display.InlineFlex` `Display.Contents` |
| `Overflow` | `Overflow.Visible` `Overflow.Hidden` `Overflow.Auto` `Overflow.Scroll` |

### Responsive — one design, every width   {#responsive}
Declare your breakpoints as tokens, then override any style prop at any of them:

```osy title="a breakpoint override" test app=ui-theming-responsive
theme Admin {
  Breakpoints { Compact = 640; Cozy = 960; }
}

enum RailState { Closed, Open }

component Sidebar(RailState rail) {
  variants {
    base {
      Position = Position.Fixed; TranslateX = "-100%";     // a phone: an off-screen drawer
      Cozy { Position = Position.Static; TranslateX = "0"; }   // ≥ 960px: a docked rail
    }
    rail { Open { TranslateX = "0"; } }
  }
  render { Text("Sidebar"); }
}
```

Styling is **mobile-first**. `base` is unconditional — it is what the narrowest screen gets — and each breakpoint
**adds** styling on top of it as the screen grows. Write it this way round and a phone never pays to undo a desktop
layout it was never going to use.

A breakpoint block may contain any style props, and may nest a pseudo-state (`Cozy { Hover { … } }`). Naming a
breakpoint you didn't declare is a compile error, with a suggestion.

**Which width?** For a component, the browser window. A component decides *placement* — is this a sidebar or a
drawer — and placement is a property of the page, not of the box the component happens to sit in. A **foreign
control** is the opposite: it reflows its own interior against the size of its own mount element, so the same grid
becomes cards whether it is narrow because the phone is narrow or because you put it in a narrow panel. Your
breakpoint tokens are visible to controls as CSS variables, so both use the same numbers.

### There is no drawer or modal — building chrome from tokens   {#chrome}
The platform ships no drawer, no modal, no sticky header. It ships the vocabulary, so you build the one you want —
and the theme still owns the **policy**: which layer things stack on, how fast they move.

```osy title="layering and timing as tokens" test app=ui-theming-chrome
theme Admin {
  ZIndex { Overlay = 20; }
  Motion { Slide = "transform 0.18s ease"; }
}

enum RailState { Closed, Open }

component Sidebar(RailState rail) {
  variants {
    base { Position = Position.Fixed; Inset = 0; Right = 0; W = 280; Overflow = Overflow.Auto;
           Z = ZIndex.Overlay; Transition = Motion.Slide; TranslateX = "-100%"; }
    rail { Open { TranslateX = "0"; } }
  }
  render { Text("Sidebar"); }
}
```

`Z = Overlay` and `Transition = Slide` are ordinary token references, so layering and timing stay consistent
across every piece of chrome in the app — the same reason colors do.

## See also   {#see-also}
- [color palettes](https://osysharp.com/reference/ui/palette/) — turn one seed color into a full ramp with `Palette.From` and its named/numbered steps.
- [layout primitives](https://osysharp.com/reference/ui/layout/) — the layout primitives (`gap`/`align`/`justify`) you arrange components with.
- [component](https://osysharp.com/reference/ui/component/) — declaring a component and its `render` block.
