Skip to content

Foundations

Design tokens

A design token is a named design decision: --fgColor-accent is “the colour of accent text”, and the theme decides which hex that is. tenet-ui has 104 tokens in one source file, tokens/tokens.json, written in the DTCG 2025.10 format. The build turns it into CSS custom properties (tenet-ui/styles.css, tenet-ui/tokens.css) and a flat registry for tools (tenet-ui/tokens.json). The token reference lists every token with its tier, value and description.

Layer
Foundation
Rules
6 numbered
Examples
6 do and don’t
Updated

104 tokens in three tiers

39baseRaw values with no meaning
61semanticDecisions with a meaning
4componentDecisions for one family

One chain, base to component

  1. Base--base-color-clay-500#c2502e
  2. Semantic--bgColor-accent-emphasisSolid accent background
  3. Component--control-medium-size32px tall, primary fill
Counts are read from the token registry. The button is a real tenet-ui Button.

Every token carries its tier in $extensions.tenet.tier. The tier says what a token is for, so it decides who may use it.

TierWhat it holdsGroupsCountWho uses it
baseRaw values with no meaning: a palette step, a sizebase.color.*, base.size.*39Semantic tokens alias them. App code never uses them.
semanticA decision with a meaning: “default text”, “danger background”, “page section gap”fgColor, bgColor, borderColor, shadow, space, fontFamily, fontSize, fontWeight, lineHeight, borderRadius, borderWidth, breakpoint61App code and components. Start here.
componentA decision that belongs to one family of componentscontrol.*.size, letterSpacing.tag4Components; app code only to line a custom element up with controls.

One chain, read from the top:

StepLightDark
App code writes var(--fgColor-accent)
Semantic token themes.<theme>.fgColor.accent{base.color.clay.600}{base.color.clay.300}
Base token#a23e1f#e9a487

The semantic name stays the same in both themes; only the alias behind it changes. See Theming.

  1. App code uses semantic tokens first. It never references a --base-* token: base values have no meaning, so nothing tells you whether --base-color-clay-600 is still the right colour after a theme change.
  2. A token is chosen by meaning, not by value. If --bgColor-default and --fgColor-onEmphasis are both white, the role decides which one a property uses; matching the hex is not enough.
  3. A value with no token is a design question, not a literal. Use the nearest token on the scale, or raise a new token; never hardcode the value.
  4. Not every semantic token aliases a base token. The type, radius, border-width, shadow and breakpoint scales hold their values directly, and 9 light and 12 dark colours (the soft tints, the backdrops and the dark surfaces) are literals. The tier is about use, not about whether the value is an alias.
  5. Components are the one exception to rule 1, and only for sizes. 22 of the 41 component stylesheets use --base-size-* directly for fixed widths and heights (icon boxes, avatars, checkboxes, tag and badge heights, progress tracks, skeleton blocks), because there are no semantic size tokens for those yet. SegmentedControl also uses one for padding, and the Switch thumb uses --base-color-white.
  6. Token names are a public API. CSS, the registry, eslint-plugin-tenet-ui and the design-system MCP server all read them, so a name only changes through the deprecation process in Versioning.

A CSS variable name is the token’s path joined with -. For themed tokens the themes.<theme> prefix is dropped, which is why both themes define the same names.

PatternExamplesUsed for
{property}-{role} and {property}-{role}-{emphasis}--fgColor-muted, --bgColor-danger-emphasis, --borderColor-accent-mutedColour. property is fgColor, bgColor or borderColor; emphasis is emphasis (solid) or muted (tint).
{scale}-{step} with numbers--space-1 to --space-6, --fontSize-0 to --fontSize-5Ordered scales. A higher number is larger.
{scale}-{size} with t-shirt sizes--borderRadius-medium, --control-small-size, --breakpoint-lgSmall sets where the names read better than numbers.
{scale}-{name}--fontWeight-semibold, --lineHeight-tight, --shadow-resting, --fontFamily-displayValues with an obvious name.
base-{kind}-{family}-{step}--base-color-clay-500, --base-size-16Tier 1 only.

tokens/tokens.json follows the DTCG 2025.10 Format and Color modules. Every token has $value and $type; most have $description; references use curly braces.

"space": {
"4": { "$value": "{base.size.16}", "$type": "dimension", "$description": "16px" }
},
"base": {
"size": {
"16": { "$value": { "value": 16, "unit": "px" }, "$type": "dimension" }
},
"color": {
"clay": {
"500": { "$value": { "colorSpace": "srgb", "components": [0.7608, 0.3137, 0.1804], "hex": "#c2502e" }, "$type": "color" }
}
}
}
TypeValue format in the sourceSerialised to CSS as
color{ colorSpace: "srgb", components, alpha?, hex }#c2502e, or rgba(...) when alpha is below 1
dimension{ value, unit }, unit px or rem16px (0 for zero)
numbera JSON number1.5
fontWeighta number from 1 to 1000600
fontFamilyan array of family names, most preferred first'Iowan Old Style', Georgia, serif
shadow{ color, offsetX, offsetY, blur, spread }0 1px 2px rgba(31, 27, 22, 0.06)

scripts/build-tokens.mjs checks every value against these shapes and fails the build if one is wrong, so the source cannot drift back to plain strings. The outputs (dist/tokens.css, dist/tokens.json) stay plain CSS strings, so tools that read them do not need a DTCG parser.

Where tenet-ui departs from the spec’s recommendations (none of these are requirements):

  • Themes are two groups in one file (themes.light, themes.dark) rather than a separate Resolver module document.
  • The vendor key in $extensions is tenet, not a reverse-domain name.
  • The file is tokens.json, not tokens.tokens.json.
  • letterSpacing.tag is stored as 0.48px. The design is 0.04em, but DTCG dimensions allow only px and rem; every use is at --fontSize-0 (12px), where the two are equal.

Do

color: var(--fgColor-muted);

A semantic token, so it follows the theme

gap: var(--space-4);

A step on the space scale

height: var(--control-medium-size);

A custom toolbar slot that lines up with medium controls

Don’t

color: var(--base-color-neutral-650);

A base token in app code; it will not change with the theme

background: var(--fgColor-onEmphasis);

White, but chosen because of the hex: --bgColor-default is the background role

padding: 14px;

Off the scale; use --space-3 or --space-4, or ask for a token