Tokens should name decisions

A token system became useful only when its names expressed maintained visual roles instead of preserving a palette as numbered aliases.

My first design tokens made the stylesheets more consistent and the design less understandable.

I extracted repeated values from five applications and gave them orderly names: blue-500, gray-700, space-3, radius-2. Replacing scattered literals with variables reduced obvious drift. A search could show where a palette value appeared. Updating a repeated gap no longer required editing several files.

Then I changed blue-500.

The same blue served as a link, primary button background, selected navigation marker, chart series, and focus indicator. Darkening it improved white text on buttons and made inline links harder to distinguish from body text in one theme. The token had centralized a value while hiding five separate decisions.

The problem was not that raw tokens were always useless. They were useful ingredients. I had exposed ingredients as the application contract and called the result a system.

J05N needed names that survived a palette change because they described why a value existed: foreground-link, action-primary-background, selection-indicator, data-series-a, focus-ring. A semantic token was not a poetic alias. It was a maintained decision about role, state, contrast, and composition.

Numbered scales described storage

The original palette followed the common scale pattern:

blue-100
blue-300
blue-500
blue-700
blue-900

The numbers ordered values from light to dark. They said nothing about whether a value belonged to text, a surface, a control, or data visualization. Consumers chose whichever value looked right nearby.

This produced consistency at the level of available paint and drift at the level of meaning. Two muted texts used different grays. A disabled button and secondary caption shared one gray despite needing different relationships to their backgrounds. Focus indicators changed by component because authors selected from the scale independently.

The scale also made change deceptively broad. Replacing gray-700 altered every role that happened to use it, even when only secondary text needed adjustment.

I kept the palette internally because theme construction still needed reusable values. Application code stopped depending on it directly. A mapping layer connected roles to ingredients:

palette.gray700 -> foreground-default
palette.gray500 -> foreground-muted
palette.blue600 -> action-primary-background
palette.blue700 -> foreground-link
palette.blue400 -> focus-ring

The mapping made reuse explicit rather than accidental. Two roles could share a value today and diverge tomorrow without renaming every consumer.

Raw scales described how values were organized. Semantic tokens described the contract applications were allowed to rely on.

A good name included context

My first semantic names were too broad:

primary
secondary
muted
danger

primary could mean text, action, brand color, or surface. danger could mean a border, destructive button, error message, or chart threshold. The missing context returned as suffixes invented locally.

J05N used a small naming grammar:

category-role-state

Examples included:

foreground-default
foreground-muted
foreground-link-hover
surface-default
surface-raised
border-control
border-control-focus
action-primary-background
action-primary-foreground
state-danger-foreground
state-danger-surface

Not every name used every segment. The grammar existed to answer useful questions: what kind of property is this, what role does it serve, and which state changes its decision?

Names avoided component identity when the role could be shared. button-blue would couple the token to one component and one current palette. action-primary-background could support a button, menu action, or another action surface whose behavior justified the same hierarchy.

Names also avoided abstract enthusiasm. brand-energy might sound expressive and leave a maintainer unable to decide where it belonged.

A token name was successful when a consumer could choose it from intent and a maintainer could change its value without guessing which unrelated uses would break.

Roles needed paired responsibilities

A foreground color is only meaningful against a background. My first semantic layer named colors independently and allowed any combination:

foreground-muted on surface-danger
action-primary-foreground on surface-raised

Some combinations failed contrast or produced the wrong hierarchy.

J05N documented supported pairs. An action role included both background and foreground. A danger state included foreground, surface, and border roles tested together. A raised surface named the foreground roles intended to appear on it.

This did not require one compound token object in CSS. It required the system to maintain relationships:

action-primary-background
  pairs with action-primary-foreground
  supports default, hover, active, disabled
  valid on surface-default and surface-raised

Contrast was evaluated at those pairs, including interaction states. A default button passing while its hover text failed was not acceptable. Focus indicators needed contrast against both the component and adjacent surface because they often straddled the edge.

The relationships also prevented a theme from mapping roles independently into an incoherent set. A dark theme could not simply invert every palette step and assume that state distinctions survived.

Tokens remained scalar values at runtime. Their documentation and tests formed a graph of responsibilities.

State belonged in the role

Several components generated hover and active values by darkening a base color in CSS or JavaScript. That seemed efficient until themes changed.

A fixed darkening operation assumes that darker always communicates interaction and remains distinguishable. On a dark surface, a lighter hover might be appropriate. A danger action might need a change in border and surface rather than the same arithmetic used by a primary action.

J05N named maintained states:

action-primary-background
action-primary-background-hover
action-primary-background-active
action-primary-background-disabled

The names looked verbose. They made state a decision the theme had to fulfill.

Disabled state exposed another issue. Reducing opacity on the whole control changed text, border, icon, and any loading indicator together, and the result depended on whatever surface sat behind it. Explicit disabled roles produced more predictable contrast. The semantic disabled state still needed native or ARIA behavior; a token could only express presentation.

Focus was never derived from brand color. focus-ring existed as a dedicated role with contrast and visibility requirements. This prevented a palette refresh from quietly erasing keyboard position.

State tokens increased the mapping surface and reduced the amount of component-specific color invention.

Spacing tokens needed semantic restraint

Color made the role problem obvious. Spacing appeared safer. I created space-1 through space-8 and allowed components to choose freely.

The scale was useful for implementation, but it did not answer who owned the gap.

A field should own the relation among label, control, description, and error. A form layout should own the gap between fields. A button should own the gap between icon and label. A toolbar should own the gap among buttons. When a child used space-3 as an external margin because it looked right in one page, composition became unpredictable.

J05N introduced semantic spacing only for stable relationships:

control-label-gap
control-description-gap
inline-icon-gap
stack-section-gap
dialog-content-gap

Generic layout utilities could still use a constrained raw scale, but component contracts documented which layer chose the value. Not every repeated 12-pixel measurement became a permanent token. A token entered the semantic layer when several consumers shared a relationship I intended to maintain together.

This prevented naming accidental layout as policy. sidebar-left-padding did not become a global decision merely because two old pages copied it.

Semantic spacing was less about replacing numbers and more about assigning responsibility for the space between things.

Typography tokens separated role from element

The first type scale named sizes: font-14, font-16, font-24. Components chose a size and weight directly. Headings began to look consistent while their document hierarchy remained inconsistent.

J05N separated semantic typography from HTML semantics. Tokens such as text-body, text-caption, text-control, and text-display described visual roles. The page still selected the appropriate heading element from document structure.

A card title could use a compact visual role and be an h2 or h3 depending on its context. The component did not hard-code a heading level because a type token sounded hierarchical.

Each typography role mapped more than font-size:

font family
size
line height
weight
letter spacing when justified

Line height mattered for zoom, wrapping, and controls with multiline labels. A type role tested with long strings and mixed scripts could not assume the single-line English sample in the catalogue.

The distinction also made hierarchy changes safer. Adjusting the visual emphasis of text-section-title did not require changing document semantics. Correcting a heading level did not unexpectedly change appearance.

Tokens named presentation decisions. They did not replace HTML's information structure.

CSS custom properties changed where mapping could happen

CSS custom properties were becoming practical in modern browsers, which made semantic roles more flexible. A theme could define mappings at a root and components could consume them without a build-time rewrite:

:root {
  --foreground-default: #252a31;
  --surface-default: #ffffff;
  --focus-ring: #2d6cdf;
}
 
.button {
  color: var(--action-primary-foreground);
  background: var(--action-primary-background);
}

My support matrix still included browsers where custom properties were absent or insufficiently dependable. J05N emitted a static declaration before the custom-property declaration:

.button {
  color: #ffffff;
  color: var(--action-primary-foreground, #ffffff);
  background: #2459a9;
  background: var(--action-primary-background, #2459a9);
}

Older browsers received the stable baseline theme. Capable browsers could apply alternate mappings. The fallback was generated from the same source to prevent hand-maintained divergence.

This was progressive enhancement, not a claim that every browser received runtime theming. Access to content, controls, focus indication, and valid contrast remained baseline requirements. Live theme switching was an enhancement.

Custom properties changed when values could be selected. They did not decide which semantic roles the system needed.

Theme mapping was a constrained compilation step

The first alternate theme was built by replacing palette values until screenshots looked plausible. It passed the overview page and failed state combinations elsewhere.

J05N treated a theme as an implementation of a token contract. A small compiler verified that every required semantic role had a value, no application-facing role referenced a missing palette entry, and known pairs met contrast checks where an automated calculation was meaningful.

The output contained:

theme identifier and version
semantic role mappings
static fallback CSS
custom-property declarations
contrast report for supported pairs
unresolved manual review scenarios

Automated contrast calculations did not replace visual and interaction review. They could not decide whether hierarchy remained understandable or whether adjacent states were distinguishable enough in context. They caught missing and objectively failing combinations early.

The compiler also prevented silent fallback. A new required token caused an alternate theme build to fail instead of inheriting a base value that might reveal the wrong brand or contrast decision.

Themes became typed against the semantic surface, even before J05N adopted TypeScript internally. Their validity depended on fulfilling decisions rather than containing a vaguely similar palette.

Migration could not be a global replacement

Moving from blue-500 to semantic roles required understanding each use. A text search found candidates but could not choose their meaning.

I generated an inventory grouped by selector and property:

blue-500 as color
blue-500 as background-color
blue-500 as border-color
blue-500 inside SVG fill

Each occurrence was classified. A link became foreground-link. A primary action surface became action-primary-background. A focus outline became focus-ring. A decorative chart mark remained a data palette value rather than pretending to be an action.

The migration happened route by route. Raw tokens remained behind deprecated aliases while consumers moved. A lint rule prevented new application use of palette tokens outside the theme layer. Exceptions required a short reason and an owner.

Some uses remained literal by design. A generated image preview could contain a project-specific color not governed by J05N. A one-off illustration did not need a semantic application token.

The slow classification was the work. A global replacement would have preserved the very coupling semantic names were meant to remove.

Aliases required direction and expiry

Renaming tokens can be more disruptive than renaming props because values flow through stylesheets, generated assets, and visual tests. J05N used aliases during migration:

old: color-link
new: foreground-link

An alias had one direction. Old names resolved to new roles; new roles never depended on deprecated names. The generated output emitted warnings in development tooling and the documentation listed known consumers.

Aliases also had removal conditions. When no application stylesheet referenced color-link and built artifacts contained no old custom property, the alias could leave. The removal occurred in a separate release so unexpected consumers could be identified clearly.

I avoided alias chains. If blue-action became control-primary and later action-primary-background, the oldest name pointed directly to the current role during its remaining life. Chains obscure which semantic shift occurred and complicate deletion.

When a rename represented a changed meaning rather than vocabulary cleanup, an alias was unsafe. Mapping a generic danger value to state-danger-foreground would be wrong for consumers that used it as a background. Those uses required classification.

Compatibility preserved known meaning; it did not manufacture meaning that the old name never contained.

Components consumed roles, not theme knowledge

An early button implementation checked the active theme and selected different values in JavaScript. That made the component aware of theme identities and duplicated mapping logic.

J05N components consumed semantic roles only:

.primaryButton {
  color: var(--action-primary-foreground);
  background: var(--action-primary-background);
  border-color: var(--action-primary-border);
}
 
.primaryButton:hover {
  background: var(--action-primary-background-hover);
}

The theme layer decided values. The component decided which roles represented its contract. Application code did not branch on palette or theme name.

This separation allowed a high-contrast mapping to increase border distinction and focus thickness without changing component logic. Not every difference fit a scalar token; some accessibility adaptations required rule-level styles. J05N allowed theme capability classes for those cases rather than forcing every decision into a color variable.

Tokens were an interface between component intent and theme realization. They were not the entire styling architecture.

The boundary also improved tests. Component snapshots could assert semantic role use, while theme tests evaluated mappings and visual scenarios separately.

Data visualization needed a different vocabulary

T04P charts exposed the limits of interface-state tokens. A chart series was not a primary action, and reusing action colors confused interaction hierarchy.

J05N defined a small data palette with roles based on sequence and meaning:

data-series-1 through data-series-6
data-positive
data-negative
data-neutral
data-selection

Series colors were tested as a set for distinguishability and paired with line style, marker, or labels where color alone could not carry identity. Positive and negative roles were used only when the data truly had that meaning; a first series did not become positive because it happened to be green.

Charts also appeared on different surfaces, so the theme contract evaluated their contrast against chart backgrounds and grid lines. A hover highlight could not reuse focus-ring merely because both emphasized a point; keyboard focus and data selection remained separate states.

This domain-specific token group belonged in J05N because several projects shared the presentation problem, not because every token needed to be universal. Its rules were narrower than general interface color.

Semantic naming improved when categories admitted different responsibilities instead of forcing the whole product into primary, secondary, and danger.

Centralizing foreground-muted made its uses consistent. It did not prove that muting a particular sentence was appropriate. A developer could apply the correct token to important instructions and reduce their hierarchy incorrectly.

J05N documentation included use conditions and counterexamples. Muted foreground was for supplementary information that remained readable, not disabled controls, placeholder text, or errors. Danger roles represented a harmful or failed state, not any attention-worthy message. Raised surfaces represented elevation relationships, not generic boxes.

Code review still needed to assess context. Visual regression could show that a role changed appearance. Automated checks could assess certain contrast pairs. Neither could determine whether the right role had been chosen.

This was another reason to keep the semantic surface small. A huge vocabulary gives every local desire an official-sounding name and makes misuse harder to recognize.

Tokens encoded maintained options and their constraints. Judgment remained required at the point of use.

The migration was complete when application stylesheets no longer imported palette tokens, deprecated aliases were gone, and components referenced semantic roles consistently.

At that point, changing the base palette did not create one undifferentiated diff. The mapping report showed which roles changed. Visual tests could group affected states. A contrast failure named the role pair responsible. An unchanged role could keep its value even if an adjacent palette step moved.

Several raw tokens remained inside the theme implementation. That was not failure. Ingredients belong somewhere. The important result was that application code no longer confused ingredient identity with product intent.

We also deleted semantic tokens that never gained meaningful consumers. A named decision without a maintained use becomes as misleading as a raw value. Token count was not progress.

The strongest test was whether the system could change a decision without forcing unrelated decisions to move with it.

Names became an architecture of intent

The first token set answered, “Which values repeat?” J05N's semantic layer answered a harder question: “Which visual decisions should change together?”

That required role names, state names, supported pairs, ownership of spacing, separation from document semantics, theme contracts, migration classification, and deliberate exceptions. The resulting system contained more thought and fewer application-facing values.

A token such as blue-500 can be perfectly precise. It is precise about the wrong layer when a component needs to express a focus indicator. focus-ring is intentionally less tied to current appearance and more tied to maintained responsibility.

Semantic tokens did not remove the palette. They prevented the palette from becoming the public meaning of the interface.

The goal was never to replace every number and color with a longer identifier. It was to give stable names to decisions worth coordinating, so a future change could move exactly the roles it intended—and leave the rest of the product still understandable.