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.
primaryis the brand action, not the blue one. - A required token is required in both selectors
- Every theme defines every required token in
:rootand in.dark. The build fails per selector and names what is missing. - Anything not every brand needs is optional
chart-*andsidebar-*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.