Token architecture
The three tiers a token can live in, the one direction references are allowed to point, and why a component may never reach past the semantic layer.
Overview
A design token is only useful if you can change it without reading every place it is used. That property does not come from having tokens; it comes from having tiers, and from a rule about which tier is allowed to reference which.
opsinjs has three: primitives, which are raw values with no opinion; semantic roles, which name a job; and component tokens, which are the handful of decisions a single component needs and nobody else should share. The one rule that makes the structure hold is that references point in exactly one direction and never skip a tier or turn around. The direction runs from component to semantic, and from semantic to primitive.
This is often confused with the Tailwind @theme layer, which is a different
concern: @theme decides which tokens become utility classes. Some of ours do,
most do not, and the mapping is documented in
Theming → Tailwind v4.
How it works
Tier 1 primitives. A ramp step, a duration in milliseconds, a radius in rem. Named for what it is: a lightness step on a hue ramp, not a purpose. A primitive never appears in a component and never appears in an example on this site. It exists so that the semantic layer has something to point at, and so that regenerating a ramp changes one file rather than forty.
Tier 2 semantic roles. Named for the job, and this is the tier you
actually work with. --opsin-status-urgent-line is a semantic role: it says
"this is the line weight of the highest status level" and it says nothing about
which hue currently satisfies that. Roles are the stable public surface. They are
covered by the versioning policy in the same
way as the JavaScript API, because a product that styles against a custom
property has taken a dependency on its name.
Tier 3 component tokens. Scoped to one component, and only created when a
decision is genuinely local, such as the inset of a range bar's marker, which no
other component has an opinion about. A component token always resolves to a
semantic role by default, so overriding one is a local adjustment and never a
fork. These are listed per selector by <CssVariablesTable> on each component
page rather than dumped globally, because the point of a component token is that
you can find it without reading the whole system.
The reference direction is the whole architecture:
flowchart LR C["Component token --range-bar-marker-inset"] --> S["Semantic role --opsin-status-urgent-line"] S --> P["Primitive ramp step, raw duration"] C -.->|banned| P
The dotted edge is the failure this page exists to prevent. A component that reaches straight to a primitive still renders, still looks right, and quietly opts itself out of every theme, every preset and every contrast measurement. Nothing tells you until somebody derives a theme from their brand colour and one component stays the old hue.
Using it
Choosing a tier. Ask who else could plausibly want this decision. If the answer is "any component showing a status", it is a semantic role. If it is "only this one, and only because of its geometry", it is a component token. If your answer is "nobody, it is just the value I need", you have found a primitive and you should not be using it directly.
Adding a role. New semantic roles are a permanent commitment. See Adding your own tokens for the extension recipe that survives an upgrade, and Contributing tokens for the migration obligation a change to an existing role creates.
The three mistakes worth naming.
Do
background: var(--opsin-status-watch-surface) is a role, so a preset can
move it, pnpm contrast can measure it, and the CVD audit can see it.
Don’t
background: oklch(0.966 0.042 85) is the same colour today, invisible to
every tool that keeps the system honest, and wrong the moment anyone themes
the product. Use the role.
The second mistake is an alias chain: a role that points at another role that
points at a third. Two hops is a smell and three is a bug, because nobody can
predict what an override does. The third is a role named after its appearance.
That means --opsin-amber-surface rather than --opsin-status-watch-surface.
Appearance names are how a system ends up with a green "amber" token after a
redesign.
Tokens
The generated list of every token, its tier, what it controls and what consumes
it, is at Reference → Tokens; the raw
custom-property names, for consumers that do not use Tailwind, are at
Reference → CSS variables. Both are
produced by scripts/build-tokens.mts from tokens/*.json, and both are
current. This page owns no family of its own, so it prints no table here rather
than a slice of somebody else's.
Accessibility impact
Tiering is what makes accessibility measurable rather than asserted. Every
contrast figure this site publishes is a measurement of a pair of semantic
roles. The pair is a foreground role against a background role, and the
measurement is only possible because those pairs have stable names. A component
that inlines a value is excluded from pnpm contrast silently: no error, no
failure, just a surface nobody has checked.
The same applies to the user preferences, and the current state of them is worth
knowing precisely. prefers-reduced-transparency is answered in the token layer
exactly as this page argues it should be: one media query in
app/tokens.generated.css redefines the tint, alpha, blur and saturation of all
six material rungs, so every surface responds and no component has to know.
prefers-reduced-motion is answered the same way but only for the spring
easings and durations; the plain duration steps are not redefined, so a
transition timed with one of those still runs at full length. prefers-contrast
has no block in the token layer at all. The one place it is answered today is
Surface, in that component's own utilities, which is precisely the pattern
this section argues against. The gap is tracked on
Increased contrast. A component
holding its own duration or its own translucency does not respond to any of
these, and the person who set the preference gets no signal that it was ignored.
Related
- Theming → Adding your own tokens is the other half of the signpost: how to extend the tiers in your own build.
- Handbook → Styling covers the four hooks a component exposes, and which tier each of them addresses.
- Colour → Colour roles has the largest set of semantic roles in the system, and the best worked example of tier 2.
Principles
Five rules that decide every other argument in opsinjs, each stated as a prohibition you can check rather than a value you can agree with.
Layering and elevation
The fixed stacking order every overlay, sheet, popover and toast belongs to, and the health rule that nothing may ever cover the value it is talking about.