# Theming

> Package `@umriss-ui/core`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/core/theming/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/core/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/core/llms-full.txt).

Make the components look like your product (theme, colours, dark mode, CSS variables). Every colour, size, shadow and duration they draw with is a token, a `--u-…` custom property, and the tokens are the whole styling API: override one with ordinary CSS, for the application or for one region. The table lists each with its light and dark value.

Demo page: https://romanhaendler.github.io/umriss-ui/core/theming/

The library writes its CSS in three cascade layers, `umriss.tokens`, `umriss.base` and `umriss.components`. CSS outside any layer wins over every layered rule, whatever its specificity and whatever order the stylesheets load in, so one unlayered `:root { --u-color-accent: … }` in your stylesheet is the whole override: no `!important`, no import order ([ADR-0021](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0021-the-styles-load-themselves-and-touch-nothing-else.md)). Inside a layer of your own it wins only if that layer was named after the library's.

Every token with two values is written `light-dark(<light>, <dark>)` and resolves against the `color-scheme` the element inherits. The library sets `color-scheme` nowhere: your `html.dark { color-scheme: dark }`, or your theme library's, switches the components, nothing set means light, and an element with its own `color-scheme` is a region in the other mode. An override written `light-dark(…)` follows the mode; a single colour holds in both.

Style through tokens only. The tokens in the table are the styling API and change only with an entry in the changelog; the `data-*` attributes and class names in the components' markup are internal and may change in any version ([ADR-0045](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0045-tokens-are-the-styling-api-data-attributes-are-not.md)). Where no token reaches what you want to change, ask for one: a selector on the markup breaks without warning.

## Examples

### Accent for the application

One rule on `:root`, outside any cascade layer, gives the whole application its own accent: it wins over the library's tokens whatever order the stylesheets load in. Set the accent's seven tokens together, each with a light and a dark value.

```tsx
import type { CSSProperties } from "react";
import { Badge, Button, Card, CardBody, CardHeader, Checkbox, Link, Stack, Switch, Text } from "@umriss-ui/core";

/* In an application this is one rule in its own stylesheet:

     :root {
       --u-color-accent: light-dark(#4338ca, #818cf8);
       --u-color-accent-hover: light-dark(#3730a3, #a5b4fc);
       --u-color-accent-subtle: light-dark(#eef0fb, #1e1f3d);
       --u-color-accent-subtle-pressed: light-dark(#dfe2f7, #282a52);
       --u-color-accent-text: light-dark(#4338ca, #a5b4fc);
       --u-color-on-accent: light-dark(#ffffff, #1e1b4b);
     }

   Here it is set on the example's root, so the rest of the page keeps the
   library's petrol. */
const INDIGO = {
  "--u-color-accent": "light-dark(#4338ca, #818cf8)",
  "--u-color-accent-hover": "light-dark(#3730a3, #a5b4fc)",
  "--u-color-accent-subtle": "light-dark(#eef0fb, #1e1f3d)",
  "--u-color-accent-subtle-pressed": "light-dark(#dfe2f7, #282a52)",
  "--u-color-accent-text": "light-dark(#4338ca, #a5b4fc)",
  "--u-color-on-accent": "light-dark(#ffffff, #1e1b4b)",
} as CSSProperties;

export default function AccentForTheApplication() {
  return (
    <div style={INDIGO}>
      <Card>
        <CardHeader
          eyebrow="Carrow & Lisle"
          title="Invoice INV-2026-0314"
          actions={<Badge tone="accent">Awaiting approval</Badge>}
        />
        <CardBody>
          <Stack gap={3} align="flex-start">
            <Text size="sm" tone="secondary">
              Marketing · 3 lines · due 31/03/2026 · <Link href="#/theming">Open the approval</Link>
            </Text>
            <Switch label="Remind the approver" defaultChecked />
            <Checkbox label="Copy the account owner" defaultChecked />
            <Button variant="ghost" size="sm">
              Approve
            </Button>
          </Stack>
        </CardBody>
      </Card>
    </div>
  );
}
```

### A token for one region

A token is a custom property and cascades like one: set on a container, it holds for everything inside and nowhere else. The second card sets its own accent, with a light and a dark value.

```tsx
import type { CSSProperties } from "react";
import { Card, CardBody, CardHeader, Checkbox, Grid, Stack, Switch } from "@umriss-ui/core";

/* In an application this is one rule on the region's class:

     .billing {
       --u-color-accent: light-dark(#7a3e9d, #b98ad6);
     }                                                    */
const BILLING = { "--u-color-accent": "light-dark(#7a3e9d, #b98ad6)" } as CSSProperties;

function Settings({ title }: { title: string }) {
  return (
    <Card>
      <CardHeader title={title} />
      <CardBody>
        <Stack gap={3}>
          <Switch label="Send a weekly summary" defaultChecked />
          <Checkbox label="Copy the account owner" defaultChecked />
        </Stack>
      </CardBody>
    </Card>
  );
}

export default function TokenForOneRegion() {
  return (
    <Grid columns={2} gap={4}>
      <Settings title="Notifications" />
      <div style={BILLING}>
        <Settings title="Billing" />
      </div>
    </Grid>
  );
}
```

### A dark region

The tokens follow the `color-scheme` an element inherits, and the library sets it nowhere: one element with `color-scheme: dark` makes everything inside it dark, on a light page too. The first tour follows the page; the second is always dark.

```tsx
import { Badge, Card, CardBody, CardHeader, Grid, Text } from "@umriss-ui/core";

/* The application's own switch is one CSS line, which its native controls
   need anyway:

     html.dark { color-scheme: dark; }
     :root { color-scheme: light dark; }  (or: follow the system)

   Nothing set means light. A region is the same line on its element:

     .dispatch-board { color-scheme: dark; background: var(--u-color-bg); } */
function Tour({ scheme }: { scheme?: "dark" }) {
  return (
    <div style={{ colorScheme: scheme, background: "var(--u-color-bg)", padding: 16, borderRadius: 8 }}>
      <Card>
        <CardHeader eyebrow="North depot" title="Tour T-03" actions={<Badge tone="warning">2 stops late</Badge>} />
        <CardBody>
          <Text size="sm" tone="secondary">
            FP 214 K · Martin Hale · 14 stops · 86 km
          </Text>
        </CardBody>
      </Card>
    </div>
  );
}

export default function ADarkRegion() {
  return (
    <Grid minItemWidth="240px" gap={4}>
      <Tour />
      <Tour scheme="dark" />
    </Grid>
  );
}
```

## Tokens

Every token the components draw with, grouped as the stylesheet groups them. A swatch is drawn from the token itself, light in the light column and dark in the dark one; "same" means one value for both themes.

### Neutral: Ink & Paper

| Token | Light | Dark | Description |
|---|---|---|---|
| `--u-color-bg` | `#fafafa` | `#0f0f10` | — |
| `--u-color-surface` | `#ffffff` | `#161618` | — |
| `--u-color-surface-sunken` | `#f4f4f5` | `#1e1e21` | — |
| `--u-color-surface-pressed` | `#e9e9eb` | `#28282c` | The pressed step of a quiet key: what sinks to the sunken surface under the pointer sinks once more while it is pressed (the interaction-state canon). The three `-pressed` surfaces exist because a hover for each ground needs something stronger. |
| `--u-color-text` | `#171717` | `#ededed` | — |
| `--u-color-text-secondary` | `#565656` | `#a5a5ab` | — |
| `--u-color-text-muted` | `#8b8b8b` | `#77777e` | — |

### Edges as shadows (instead of borders)

| Token | Light | Dark | Description |
|---|---|---|---|
| `--u-edge-color` | `rgba(23, 23, 23, 0.08)` | `rgba(255, 255, 255, 0.10)` | — |
| `--u-edge-color-strong` | `rgba(23, 23, 23, 0.17)` | `rgba(255, 255, 255, 0.20)` | — |
| `--u-edge` | `0 0 0 1px var(`[--u-edge-color](#token-u-edge-color)`)` | same | — |
| `--u-edge-strong` | `0 0 0 1px var(`[--u-edge-color-strong](#token-u-edge-color-strong)`)` | same | — |
| `--u-hairline` | `rgba(23, 23, 23, 0.06)` | `rgba(255, 255, 255, 0.07)` | inner dividing lines |
| `--u-color-edge-hover` | `color-mix(in srgb, var(`[--u-color-text](#token-u-color-text)`) 30%, transparent)` | same | The edge of a field under the pointer. |
| `--u-hairline-strong` | `rgba(23, 23, 23, 0.12)` | `rgba(255, 255, 255, 0.14)` | e.g. under table heads |

### Ink as interaction (the primary button)

| Token | Light | Dark | Description |
|---|---|---|---|
| `--u-color-primary-bg` | `#171717` | `#ededed` | — |
| `--u-color-primary-fg` | `#ffffff` | `#171717` | — |
| `--u-color-primary-hover` | `#2e2e2e` | `#ffffff` | — |
| `--u-color-primary-active` | `#000000` | `#d6d6d6` | — |

### Accent (petrol) - interactive things only

| Token | Light | Dark | Description |
|---|---|---|---|
| `--u-color-accent` | `#0e7d72` | `#17a292` | — |
| `--u-color-accent-hover` | `#0a685f` | `#1cb5a3` | — |
| `--u-color-accent-subtle` | `#e9f4f2` | `#142e2b` | — |
| `--u-color-accent-subtle-pressed` | `#d5e9e6` | `#1b3d39` | — |
| `--u-color-accent-text` | `#0b6a61` | `#4cc4b5` | — |
| `--u-color-on-accent` | `#ffffff` | `#072522` | — |
| `--u-color-on-danger` | `#ffffff` | `#2b0f0d` | Type on the danger surface, a token so that a contrast check sees it. Dark: White measures only 3.68:1 on the lighter danger surface. The polarity turns as it does for the accent: dark type on a light ground. |

### Semantics (muted)

| Token | Light | Dark | Description |
|---|---|---|---|
| `--u-color-success` | `#217a4b` | `#4fae7c` | — |
| `--u-color-success-subtle` | `#e8f2ec` | `#16241d` | — |
| `--u-color-warning` | `#8a5c00` | `#d0a24d` | Darkened from #9a6700, which reached only 4.24:1 on its own pale surface and so missed the 4.5:1 for normal text; this reaches 4.97:1. |
| `--u-color-warning-subtle` | `#f7efdb` | `#2b2415` | — |
| `--u-color-danger` | `#b13636` | `#d0655c` | — |
| `--u-color-danger-hover` | `#9a2c2c` | `#da7a72` | — |
| `--u-color-danger-active` | `color-mix(in srgb, var(`[--u-color-danger](#token-u-color-danger)`) 85%, #000000)` | `color-mix(in srgb, var(`[--u-color-danger](#token-u-color-danger)`) 85%, #ffffff)` | Pressed: darker in the light theme. As a mixture rather than a value, so that it follows the ground; the dark theme turns the direction round. |
| `--u-color-danger-subtle` | `#f8eaea` | `#2f1b1d` | — |
| `--u-color-danger-subtle-pressed` | `#f3dcdc` | `#351e21` | — |
| `--u-color-danger-text` | `#b13636` | `#d86f66` | Danger as type, beside the surface - the split the accent has with --u-color-accent-text. Dark: the surface value reached only 4.40:1 on the subtle danger surface; this one is lighter and holds 4.5:1 there and on every ground (tone-contrast). Light: the surface value already passes. |

### Focus: a crisp accent edge, two pixels, no glow

One look for every component. A soft glow would read as a second, shadowy border. On a field the ring lies where its edge lies, so the edge seems to grow. `-danger` for an invalid field, which keeps its colour.

| Token | Light | Dark | Description |
|---|---|---|---|
| `--u-focus-ring` | `0 0 0 2px var(`[--u-color-accent](#token-u-color-accent)`)` | same | — |
| `--u-focus-ring-danger` | `0 0 0 2px var(`[--u-color-danger](#token-u-color-danger)`)` | same | — |

### Typography

| Token | Light | Dark | Description |
|---|---|---|---|
| `--u-font-sans` | `"Geist Sans", "Geist", -apple-system, "Segoe UI", system-ui, sans-serif` | same | — |
| `--u-font-mono` | `"Geist Mono", ui-monospace, "Cascadia Mono", "Consolas", monospace` | same | — |
| `--u-text-xs` | `0.6875rem` | same | — |
| `--u-text-sm` | `0.8125rem` | same | — |
| `--u-text-md` | `0.875rem` | same | — |
| `--u-text-lg` | `1rem` | same | — |
| `--u-text-xl` | `1.1875rem` | same | — |
| `--u-text-2xl` | `1.5rem` | same | — |
| `--u-text-mono` | `0.75rem` | same | Figures in Geist Mono inside a control or a cell, beside --u-text-sm set in Sans. The mono cut runs wider and taller, so one step down reads as the same size. It serves as a role: the date picker's value, days and time fields, the numeric field and input, the table's numeric cells. |
| `--u-leading-none` | `1` | same | A box as tall as its type: a badge, a tag, a count, a glyph. The height comes from the control or the padding, and the line adds no leading. |
| `--u-leading-tight` | `1.3` | same | — |
| `--u-leading-normal` | `1.5` | same | — |
| `--u-weight-regular` | `400` | same | — |
| `--u-weight-medium` | `500` | same | — |
| `--u-weight-semibold` | `600` | same | — |
| `--u-tracking-caps` | `0.07em` | same | — |
| `--u-tracking-display` | `-0.022em` | same | large titles: slightly compressed |
| `--u-tracking-tight` | `-0.011em` | same | medium titles (cards, dialogs) |
| `--u-tracking-figures` | `-0.01em` | same | figures set in the mono face |

### Spacing and shape

| Token | Light | Dark | Description |
|---|---|---|---|
| `--u-space-1` | `4px` | same | — |
| `--u-space-2` | `8px` | same | — |
| `--u-space-3` | `12px` | same | — |
| `--u-space-4` | `16px` | same | — |
| `--u-space-5` | `20px` | same | — |
| `--u-space-6` | `24px` | same | — |
| `--u-space-7` | `32px` | same | — |
| `--u-space-8` | `40px` | same | — |
| `--u-radius-xs` | `4px` | same | — |
| `--u-radius-sm` | `6px` | same | — |
| `--u-radius-md` | `8px` | same | — |
| `--u-radius-lg` | `12px` | same | — |
| `--u-radius-xl` | `18px` | same | One step above the card, for the command palette. It is meant to be a system window and not a form panel, and the radius plays its part in that. Both themes share it - shape is not a question of theme. The sequence 4-6-8-12-18 continues the factor of 1.5 of the steps above. |
| `--u-radius-full` | `999px` | same | — |
| `--u-control-height` | `32px` | same | — |
| `--u-control-height-sm` | `26px` | same | — |
| `--u-control-height-xs` | `18px` | same | The small control on a dense surface - a compact table's row draft and actions (`dense` in the shared own styles). |
| `--u-icon-size` | `14px` | same | The icon in a button, beside a label or alone (CONTEXT.md, "Icon"): the button decides its size, one step per control height. |
| `--u-icon-size-sm` | `12px` | same | — |
| `--u-button-padding` | `10px` | same | The room left and right of a button's label, one step per control height. What stands beside a quiet button and has to align with its word hangs out by exactly this much (the date pickers' foot, the MultiSelect's quick keys) - so they read it here instead of repeating the number. |
| `--u-button-padding-sm` | `6px` | same | — |
| `--u-drawer-width` | `400px` | same | The width of a drawer A token rather than a size prop: a detail beside a process picture wants one width across an application, and one drawer that needs another sets it in its `style`. |

### Toast

How far the toasts stand from the window's top and bottom edge and from its sides - set it to keep them clear of a fixed header, a toolbar at the bottom or a navigation rail at the side - and how wide a toast is. The region hangs in `body`, so set them on `:root`; a media query of your own may set another value per width. The center positions stand in the middle of the window whatever the side tokens are.

| Token | Light | Dark | Description |
|---|---|---|---|
| `--u-toast-inset-top` | `var(`[--u-space-5](#token-u-space-5)`)` | same | for the three `top-*` positions; `var(--u-space-4)` up to 600 px wide |
| `--u-toast-inset-bottom` | `var(`[--u-space-5](#token-u-space-5)`)` | same | for the three `bottom-*` positions; `var(--u-space-4)` up to 600 px wide |
| `--u-toast-inset-start` | `var(`[--u-space-5](#token-u-space-5)`)` | same | for the `*-start` positions, and on a phone the room at the start side; `var(--u-space-4)` up to 600 px wide |
| `--u-toast-inset-end` | `var(`[--u-space-5](#token-u-space-5)`)` | same | for the `*-end` positions, and on a phone the room at the end side; `var(--u-space-4)` up to 600 px wide |
| `--u-toast-width` | `360px` | same | a plain length, read it to know whether the deck has room beside a surface; up to 600 px wide the toast takes the window's width less the side tokens instead |

### Shadow stacks: every step has one job

Card: edge, contact with the ground, soft depth. Overlay: edge, near shadow, depth.

| Token | Light | Dark | Description |
|---|---|---|---|
| `--u-shadow-card` | `var(`[--u-edge](#token-u-edge)`), 0 1px 2px rgba(23, 23, 23, 0.04), 0 8px 24px -12px rgba(23, 23, 23, 0.14)` | `var(`[--u-edge](#token-u-edge)`), 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px -12px rgba(0, 0, 0, 0.6)` | — |
| `--u-shadow-overlay` | `var(`[--u-edge](#token-u-edge)`), 0 4px 8px rgba(23, 23, 23, 0.06), 0 24px 48px -16px rgba(23, 23, 23, 0.24)` | `var(`[--u-edge](#token-u-edge)`), 0 4px 8px rgba(0, 0, 0, 0.4), 0 24px 48px -16px rgba(0, 0, 0, 0.7)` | — |
| `--u-shadow-sticky` | `rgba(23, 23, 23, 0.09)` | `rgba(0, 0, 0, 0.5)` | The shadow a sticking part casts on what scrolls under it - a pinned column block, a stuck group header - as the colour its gradient starts from. Dark is black at a higher opacity: the text colour at 9 % read as a glow on the dark ground. |

### Translucent material and scrim ([ADR-0012](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0012-a-translucent-material-needs-a-floor.md))

An overlay meant to be a window ABOVE the application and not a card WITHIN it needs two things: the page behind it must be sensed, and it must never stay readable.

The alpha value is a FLOOR and not a matter of taste: the opaque colour carries legibility on its own, the transparency delivers atmosphere and never contrast. A panel that looks prettier at 0.55 is one whose type is legible by luck. The floor is open upwards, and in the light theme it lies higher than strictly necessary - see below.

Three tokens, because there are three jobs: - `-fallback` is the opaque colour underneath. It is really painted where `backdrop-filter` is missing or switched off, and it is what the contrast test checks (see tests-unit/contrast.test.ts). - `--u-color-material` is the same colour with the floor alpha. - `--u-material-blur` softens what shows through. Decoration under the same rule: the blur does not make the type legible.

The scrim dims the page behind an overlay. Dark gets its own values, chosen by eye: inverting a light surface lands wrong, because light type over a dark blur tolerates far less transparency than the other way round.

| Token | Light | Dark | Description |
|---|---|---|---|
| `--u-color-material-fallback` | `#fdfdfe` | `#1f1f23` | The numbers were looked at, not calculated. In the light theme the scrim must dim less and the panel cover more: a scrim that presses the whole page to a middling grey leaves the panel a grey slab within it, legible but looking like a fault. The distance between the two carries the step. The dark theme needs no such correction: there the lightness already separates them. Dark: Chosen by eye, not derived from the light theme: the alpha floor lies higher here, because light type over a dark blur tolerates markedly less transparency. |
| `--u-color-material` | `rgba(253, 253, 254, 0.86)` | `rgba(31, 31, 35, 0.82)` | — |
| `--u-material-blur` | `24px` | same | — |
| `--u-color-scrim` | `rgba(23, 23, 23, 0.30)` | `rgba(0, 0, 0, 0.60)` | — |

### Motion: explains a change of state, does not decorate

The names follow the distinctions the library makes (visuelle-wertigkeit 02); a motion that fits none of them wants a new name here, not a number in its module.

Micro or path. A colour, an edge or an opacity changes in place: that is `--u-transition`. Something moves, turns or unfolds: that is `--u-transition-path`, or `--u-duration-medium` with `--u-ease-out` for a long way. A press is shorter than either.

Entry or exit. A surface enters on `--u-ease-out` from its motion origin; it leaves faster, on a curve that starts at once, and does not retrace the way it came - a symmetrical exit makes a surface feel sluggish.

Continuous process. A spinner or a shimmer has no start and no end; it runs steadily and long.

| Token | Light | Dark | Description |
|---|---|---|---|
| `--u-ease-out` | `cubic-bezier(0.22, 1, 0.36, 1)` | same | decisive, landing softly: entries and paths |
| `--u-ease-exit` | `cubic-bezier(0.2, 0.6, 0.4, 1)` | same | leaves at once, no wind-up |
| `--u-transition` | `120ms var(`[--u-ease-out](#token-u-ease-out)`)` | same | micro: hover, colour. It carried the CSS keyword `ease-out` and so kept the curve above from three quarters of the library's transitions. **0ms under reduced motion.** |
| `--u-duration-press` | `80ms` | same | the press point of a button or glyph **0ms under reduced motion.** |
| `--u-transition-press` | `var(`[--u-duration-press](#token-u-duration-press)`) var(`[--u-ease-out](#token-u-ease-out)`)` | same | — |
| `--u-duration-fast` | `140ms` | same | entry of a panel; a short path **0ms under reduced motion.** |
| `--u-transition-path` | `var(`[--u-duration-fast](#token-u-duration-fast)`) var(`[--u-ease-out](#token-u-ease-out)`)` | same | a chevron turns, a thumb slides |
| `--u-duration-medium` | `240ms` | same | entry of a sheet; a layout change **0ms under reduced motion.** |
| `--u-duration-draw` | `320ms` | same | a stroke drawn slowly enough to watch: the tick **0ms under reduced motion.** |
| `--u-delay-draw` | `60ms` | same | the box fills first, then the tick is drawn **0ms under reduced motion.** |
| `--u-delay-stale` | `200ms` | same | content being reloaded dims only after this: a quick answer never flickers |
| `--u-duration-exit-fast` | `100ms` | same | Exits. The stylesheet drawing the movement and the code waiting for its end read the same tokens, so the two cannot drift apart. a panel or a tooltip goes **0ms under reduced motion.** |
| `--u-duration-exit` | `160ms` | same | a sheet takes its leave: modal **0ms under reduced motion.** |
| `--u-duration-exit-collapse` | `200ms` | same | a message goes and its place closes **0ms under reduced motion.** |
| `--u-duration-spin` | `700ms` | same | Continuous processes. They do NOT fall to zero under reduced motion - a spinner at 0ms says nothing; each module slows or stops its own. |
| `--u-duration-shimmer` | `1.6s` | same | — |
| `--u-duration-sweep` | `1.4s` | same | an indeterminate progress bar crosses its track |
| `--u-ease-steady` | `linear` | same | a turn with no start and no end |
| `--u-ease-swell` | `ease-in-out` | same | a shimmer that comes and goes |

### Stacking order

Popovers portal into the nearest <dialog> ancestor, and this scale orders the rest. The toasts also move into the topmost modal dialog. Inside a dialog the toasts and the tooltips stand in the top layer, which orders by the moment of showing and not by these numbers: a tooltip is shown after the toasts and lies above them, as the scale has it; popovers stay under the toasts.

| Token | Light | Dark | Description |
|---|---|---|---|
| `--u-z-popover` | `900` | same | — |
| `--u-z-toast` | `1000` | same | — |
| `--u-z-tooltip` | `1100` | same | — |

## @umriss-ui/charts

The charts' own tokens, set on a chart's root. A token whose value names a core token takes that token, so theming core themes the charts; where core is not loaded, or has no such token (`--u-chart-1` to `--u-chart-6`), the literal after it stands.

| Token | Light | Dark | Description |
|---|---|---|---|
| `--uc-color-axis` | `var(`[--u-edge-color-strong](#token-u-edge-color-strong)`, rgba(23, 23, 23, 0.17))` | same | — |
| `--uc-color-grid` | `var(`[--u-hairline](#token-u-hairline)`, rgba(23, 23, 23, 0.06))` | same | — |
| `--uc-color-text` | `var(`[--u-color-text-muted](#token-u-color-text-muted)`, #71717a)` | same | — |
| `--uc-color-bg` | `var(`[--u-color-surface](#token-u-color-surface)`, #ffffff)` | same | — |
| `--uc-font` | `var(`[--u-font-sans](#token-u-font-sans)`, "Geist Sans", "Geist", -apple-system, "Segoe UI", system-ui, sans-serif)` | same | — |
| `--uc-font-mono` | `var(`[--u-font-mono](#token-u-font-mono)`, "Geist Mono", ui-monospace, "Cascadia Mono", "Consolas", monospace)` | same | — |
| `--uc-color-warning` | `var(`[--u-color-warning](#token-u-color-warning)`, #8a5c00)` | same | The two limit severities. They fall back onto the same tokens with which @umriss-ui/core warns and alarms - a limit in the chart and a tile above it must not be differently red. |
| `--uc-color-alarm` | `var(`[--u-color-danger](#token-u-color-danger)`, #b13636)` | same | — |
| `--uc-color-ok` | `var(`[--u-color-success](#token-u-color-success)`, #217a4b)` | same | — |
| `--uc-series-1` | `var(--u-chart-1, #2563eb)` | same | — |
| `--uc-series-2` | `var(--u-chart-2, #db2777)` | same | — |
| `--uc-series-3` | `var(--u-chart-3, #059669)` | same | — |
| `--uc-series-4` | `var(--u-chart-4, #d97706)` | same | — |
| `--uc-series-5` | `var(--u-chart-5, #7c3aed)` | same | — |
| `--uc-series-6` | `var(--u-chart-6, #0891b2)` | same | — |
| `--uc-size-tick` | `11.5px` | same | — |
| `--uc-size-title` | `12px` | same | — |
| `--uc-size-legend` | `12.5px` | same | — |
| `--uc-size-tooltip` | `12px` | same | The tooltip's text; its head is an x value and takes the tick's size. |
| `--uc-leading-label` | `1.25` | same | A label on an axis stands tight to its tick; running text - the legend, the tooltip - gets a little more air. |
| `--uc-leading-text` | `1.35` | same | — |
| `--uc-radius` | `6px` | same | — |
| `--uc-radius-chip` | `2px` | same | The colour chips of legend and tooltip, and the legend entry's focus. |
| `--uc-shadow` | `0 1px 2px rgba(0, 0, 0, 0.06), 0 4px 12px rgba(0, 0, 0, 0.08)` | same | — |
| `--uc-focus-ring` | `var(`[--u-focus-ring](#token-u-focus-ring)`, 0 0 0 2px #0e7d72)` | same | — |
| `--uc-color-hover` | `var(`[--u-color-surface-sunken](#token-u-color-surface-sunken)`, #f4f4f5)` | same | A legend entry under the pointer and pressed: core's quiet surfaces, and core's motion for a change in place (the interaction-state canon). |
| `--uc-color-pressed` | `var(`[--u-color-surface-pressed](#token-u-color-surface-pressed)`, #e9e9eb)` | same | — |
| `--uc-transition-state` | `var(`[--u-transition](#token-u-transition)`, 120ms cubic-bezier(0.22, 1, 0.36, 1))` | same | — |
| `--uc-transition` | `80ms var(`[--u-ease-out](#token-u-ease-out)`, cubic-bezier(0.22, 1, 0.36, 1))` | same | The tooltip's fade: its own short duration - it follows the pointer and must not lag behind it - on core's curve where core is loaded. |
| `--uc-delay-stale` | `var(`[--u-delay-stale](#token-u-delay-stale)`, 200ms)` | same | A course being reloaded dims as the table's rows do ([ADR-0042](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0042-a-rows-height-never-follows-what-it-shows.md)): only after this delay, so that a quick answer never flickers. |
| `--uc-silhouette` | `color-mix(in srgb, var(`[--u-color-text](#token-u-color-text)`, #18181b) 10%, transparent)` | same | The shapes of the silhouette a chart shows while it waits for its rows (chart-loading 03): faint in the text colour, as the table's placeholders. |
| `--uc-ease-swell` | `var(`[--u-ease-swell](#token-u-ease-swell)`, ease-in-out)` | same | The easing of the band that sweeps the silhouette: it swells as the table's shimmer does. |
| `--uc-fade` | `var(`[--u-duration-medium](#token-u-duration-medium)`, 240ms) var(`[--u-ease-out](#token-u-ease-out)`, cubic-bezier(0.22, 1, 0.36, 1))` | same | The silhouette fading out and the course in, once the rows are there: core's motion for a layout change. |

## Keyboard

The keys of [Button](https://romanhaendler.github.io/umriss-ui/core/button/#keyboard-button), [Checkbox](https://romanhaendler.github.io/umriss-ui/core/checkbox/#keyboard-checkbox), [Switch](https://romanhaendler.github.io/umriss-ui/core/switch/#keyboard-switch) and [Typography](https://romanhaendler.github.io/umriss-ui/core/typography/#keyboard-typography) apply here.

## Props on this page

From `StackProps` — the full table stands on [Stack and Grid](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#type-StackProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`gap`](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#StackProps-gap) | [`SpaceStep`](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#type-SpaceStep)<br>`1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8` | `3` | Spacing as a step of the 4 px spacing steps (1–8). |
| [`align`](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#StackProps-align) | `CSSProperties["alignItems"]` | — | Alignment across the direction – `alignItems` unchanged. |

From `GridProps` — the full table stands on [Stack and Grid](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#type-GridProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`columns`](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#GridProps-columns) | `number \| readonly `[`GridColumnWidth`](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#type-GridColumnWidth)`[]` | `2` | How many columns, all of one width - or a list with one width per column: a number in pixels, or `"fill"` for a share of what is left. Overridden by minItemWidth. |
| [`minItemWidth`](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#GridProps-minItemWidth) | `string` | — | Responsive: as many columns as fit at this minimum width; wins over `columns`, a list included. |
| [`gap`](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#GridProps-gap) | [`SpaceStep`](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#type-SpaceStep)<br>`1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8` | `4` | Spacing as a step of the 4 px spacing steps (1–8); applies in both directions. |

From `CardHeaderProps` — the full table stands on [Card](https://romanhaendler.github.io/umriss-ui/core/card/#type-CardHeaderProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`title`](https://romanhaendler.github.io/umriss-ui/core/card/#CardHeaderProps-title) *required* | `ReactNode` | — | The heading of the card. A node and not merely text, so that an identifier with markup can stand in it too. |
| [`eyebrow`](https://romanhaendler.github.io/umriss-ui/core/card/#CardHeaderProps-eyebrow) | `ReactNode` | — | Small all-caps label above the title, e.g. a category. |
| [`actions`](https://romanhaendler.github.io/umriss-ui/core/card/#CardHeaderProps-actions) | `ReactNode` | — | Right-aligned area, e.g. buttons or badges. |

From `TextProps` — the full table stands on [Typography](https://romanhaendler.github.io/umriss-ui/core/typography/#type-TextProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`size`](https://romanhaendler.github.io/umriss-ui/core/typography/#TextProps-size) | [`TextSize`](https://romanhaendler.github.io/umriss-ui/core/typography/#type-TextSize)<br>`"xs" \| "sm" \| "md" \| "lg" \| "xl" \| "2xl"` | `"md"` | Type size from the token set. It is independent of the element: a `p` may be small without becoming a different element. |
| [`tone`](https://romanhaendler.github.io/umriss-ui/core/typography/#TextProps-tone) | [`TextTone`](https://romanhaendler.github.io/umriss-ui/core/typography/#type-TextTone)<br>`"default" \| "secondary" \| "muted"` | `"default"` | How loud the text is. `secondary` and `muted` step back without falling below the contrast threshold. |

From `ButtonProps` — the full table stands on [Button](https://romanhaendler.github.io/umriss-ui/core/button/#type-ButtonProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`variant`](https://romanhaendler.github.io/umriss-ui/core/button/#ButtonProps-variant) | [`ButtonVariant`](https://romanhaendler.github.io/umriss-ui/core/button/#type-ButtonVariant)<br>`"primary" \| "secondary" \| "ghost" \| "plain" \| "danger"` | `"secondary"` | How loud the button is. `primary` exactly once per surface - two main actions side by side are no longer a main action. `ghost` is quiet in the accent, `plain` quiet and neutral. |
| [`size`](https://romanhaendler.github.io/umriss-ui/core/button/#ButtonProps-size) | [`ButtonSize`](https://romanhaendler.github.io/umriss-ui/core/button/#type-ButtonSize)<br>`"sm" \| "md"` | the size of a `ControlSizeProvider` around it, else `"md"` | `sm` for buttons in header bars and table rows, `md` otherwise. |

From `IconButtonProps` — the full table stands on [IconButton](https://romanhaendler.github.io/umriss-ui/core/iconbutton/#type-IconButtonProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`size`](https://romanhaendler.github.io/umriss-ui/core/iconbutton/#IconButtonProps-size) | [`ButtonSize`](https://romanhaendler.github.io/umriss-ui/core/iconbutton/#type-ButtonSize)<br>`"sm" \| "md"` | the size of a `ControlSizeProvider` around it, else `"md"` | `sm` for buttons in header bars and table rows, `md` otherwise. From `ButtonProps`. |

From `CheckboxProps` — the full table stands on [Checkbox](https://romanhaendler.github.io/umriss-ui/core/checkbox/#type-CheckboxProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`label`](https://romanhaendler.github.io/umriss-ui/core/checkbox/#CheckboxProps-label) | `ReactNode` | — | Label to the right of the checkbox. |

From `SwitchProps` — the full table stands on [Switch](https://romanhaendler.github.io/umriss-ui/core/switch/#type-SwitchProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`label`](https://romanhaendler.github.io/umriss-ui/core/switch/#SwitchProps-label) | `ReactNode` | — | Label to the right of the switch. |

From `BadgeProps` — the full table stands on [Badge](https://romanhaendler.github.io/umriss-ui/core/badge/#type-BadgeProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`tone`](https://romanhaendler.github.io/umriss-ui/core/badge/#BadgeProps-tone) | [`BadgeTone`](https://romanhaendler.github.io/umriss-ui/core/badge/#type-BadgeTone)<br>`"neutral" \| "accent" \| "success" \| "warning" \| "danger"` | `"neutral"` | What the badge stands for. There is always a word beside it: a colour on its own is not a statement. |

## Known limits

- No theme object, no theme provider and no theme switch: a theme is a set of token overrides in your stylesheet, the mode is the page's `color-scheme` ([ADR-0021](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0021-the-styles-load-themselves-and-touch-nothing-else.md)).
- A token that names another resolves where it is declared: set `--u-color-accent` on a region, and `--u-focus-ring`, which names it on `:root`, still draws the application's accent there. Set the naming token in the region too.
- A panel opened in a portal - a popover, a tooltip, a toast - takes the tokens and the `color-scheme` of the place it lands, not of the region it was opened from; inside a dialog the toasts take the tokens of the dialog's place. Give [UmrissProvider](https://romanhaendler.github.io/umriss-ui/core/umrissprovider/) a `portalTarget` inside the region, or override on `:root` - as for the toasts' distance from the window's edge and its sides, `--u-toast-inset-top`, `--u-toast-inset-bottom`, `--u-toast-inset-start` and `--u-toast-inset-end`.

What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/).
