Skip to content

Styling with tokens

Use the same CSS variables as the components, so your own UI matches and follows the theme.

The styled components read semantic tokens: CSS custom properties named after their role, not their value. Use them for your own layout, and it will match the components in every theme:

.panel {
  display: grid;
  gap: var(--ct-space-group-gap);
  padding: var(--ct-space-panel-inset);
  border: var(--ct-border-width-default) solid var(--ct-color-border-default);
  border-radius: var(--ct-radius-panel);
  background: var(--ct-color-bg-surface);
  color: var(--ct-color-fg-default);
  font: var(--ct-text-body);
  letter-spacing: var(--ct-text-body-letter-spacing);
}

Token groups

VariablesFor
--ct-color-bg-*backgrounds by layer: canvas, surface, raised, inset, hover
--ct-color-fg-*text and icons: default, muted, subtle, disabled
--ct-color-border-*subtle dividers, default outlines, strong control edges, focus rings
--ct-color-<status>-*accent, success, warning, danger: fill, fill-hover, on-fill, fg, bg, border
--ct-color-overlay-*scrim: the translucent backdrop behind dialogs and drawers
--ct-color-syntax-*code highlighting: constant, string, comment, keyword, parameter, function, string-expression, punctuation, link
--ct-space-*padding and gaps: control-inset, -inset-lg, control-gap, -gap-lg, group-gap, panel-inset, section-gap
--ct-size-*control heights: control-sm, -md, -lg; icon sizes: icon-sm, -md, -lg; overlay sizes: dialog-*, drawer-*, popover
--ct-radius-*control, panel, round
--ct-border-width-*default, focus
--ct-shadow-*raised for popovers and menus, overlay for dialogs and drawers
--ct-layer-*stacking order (z-index): sticky headers, overlay popovers and dialogs
--ct-text-*text styles: heading, body, label, label-lg, value, caption; long-form pages: display, title, subtitle, prose, code; site pages: hero, lead, nav
--ct-motion-*transitions: feedback, enter, exit

Tokens lists every one of them with its value in both themes and a sample.

Rules

  • Typography. --ct-text-<style> is a font shorthand. letter-spacing is not part of the shorthand: pair it with --ct-text-<style>-letter-spacing. Single values are available too: -font-family, -font-size, -font-weight, -line-height.
  • Motion. --ct-motion-<name> is a duration, easing and delay: transition: background-color var(--ct-motion-feedback). exit is shorter than enter, for things that close.
  • Shadows and layers. --ct-shadow-<name> is a box-shadow value, stronger in the dark theme. It is decoration: whatever casts it also has a border and a raised background. --ct-layer-<name> is a plain number for z-index.
  • Code. The syntax colors follow the variables of Shiki's css-variables theme: --shiki-token-<name> takes --ct-color-syntax-<name>. Code blocks sit on bg-inset.
  • Contrast. Text roles meet WCAG targets on the backgrounds they are meant for, in both themes: fg-default on canvas, surface and raised (7:1), fg-muted on surface and raised (4.5:1), every syntax color on inset (4.5:1). fg-subtle (3:1) is for units and tick labels, not body text; fg-disabled is for disabled labels and has no target.
  • Never use --ct-base-*. Those are the raw palette and scales behind the roles. They exist so roles can refer to them, are not part of the API, and change without notice.

In TypeScript

vars from @contour-ui/tokens holds a typed var(…) reference for every semantic token, for inline styles and CSS-in-JS:

import { vars } from '@contour-ui/tokens';

<div style={{ padding: vars.space['panel-inset'], background: vars.color.bg.surface }} />;
// vars.color.bg.surface === 'var(--ct-color-bg-surface)'