Skip to content

Token contract

Components name tokens; themes supply the values, once for light and once for dark. That is what lets the same button take on each project's look without being forked.

The names

43 tokens. A component may use these and nothing else. The swatches are the current theme's values, so they change with the theme toggle.

Surface7

  • backgroundPage ground.
  • foregroundPrimary text on background.
  • cardCard and panel ground.
  • card-foregroundText on card.
  • popoverFloating surfaces: popovers, menus, listboxes.
  • popover-foregroundText on popover.
  • mutedQuiet fill: table header, row hover, disabled field.

Text2

  • muted-foregroundSecondary text: labels, meta, table headers.
  • subtle-foregroundTertiary text: placeholders, captions, weekday labels. Lowest contrast that still reads.

Action6

  • primaryBrand action: primary button, selected state, links.
  • primary-foregroundText and icons on primary.
  • secondarySecondary button fill.
  • secondary-foregroundText on secondary.
  • accentHover/highlight fill for menu items and ghost controls. Not the brand colour.
  • accent-foregroundText on accent.

Line3

  • borderDefault hairline for cards, tables, dividers.
  • inputForm control border.
  • ringFocus ring.

Status12

  • destructiveErrors and destructive actions. Must read as small text on background and on its own 10% tint.
  • destructive-foregroundText on a destructive fill.
  • destructive-subtleTinted ground for error banners and tags; text on it uses destructive.
  • successPositive state. Must read as small text on background and on its own 10% tint.
  • success-foregroundText on a success fill.
  • success-subtleTinted ground for success banners and badges; text on it uses success.
  • warningNeeds-attention state. Must read as small text on background and on its own 10% tint.
  • warning-foregroundText on a warning fill.
  • warning-subtleTinted ground for warning banners and badges.
  • infoNeutral informational state. Must read as small text on background and on its own 10% tint.
  • info-foregroundText on an info fill.
  • info-subtleTinted ground for info banners and badges.

Chart5

  • chart-1optionalCategorical series 1.
  • chart-2optionalCategorical series 2.
  • chart-3optionalCategorical series 3.
  • chart-4optionalCategorical series 4.
  • chart-5optionalCategorical series 5.

Sidebar8

  • sidebaroptionalSidebar ground.
  • sidebar-foregroundoptionalSidebar text.
  • sidebar-primaryoptionalActive sidebar item.
  • sidebar-primary-foregroundoptionalText on active sidebar item.
  • sidebar-accentoptionalSidebar hover fill.
  • sidebar-accent-foregroundoptionalText on sidebar hover fill.
  • sidebar-borderoptionalSidebar divider.
  • sidebar-ringoptionalSidebar focus ring.

The rules

Names and roles belong to the contract; values belong to a theme
If a role can't be stated without naming a colour, it is a value, not a name. primary is the brand action, not the blue one.
A required token is required in both selectors
Every theme defines every required token in :root and in .dark. The build fails per selector and names what is missing.
Anything not every brand needs is optional
chart-* and sidebar-* are optional for that reason. A new required token breaks every theme that already exists.

Changing it

A component that needs a colour the contract doesn't have is a contract change, not a local exception.

bun run tokens
regenerate theme.css and TOKENS.md, and validate every theme

The generated files are not edited by hand

Change contract.json and regenerate. A hook blocks direct edits to theme.css and TOKENS.md.

Exceptions are how a lint rule turns into an allowlist of workarounds.