Skip to content
Primitiv home
Framework
Consumption mode

CONCEPTS

Tokens and theming

A token is a named design decision. Instead of writing a colour into forty stylesheets, you name it once and point at the name.

That sounds like a variable, and technically it is one. What makes tokens worth a page of their own is how they are layered. Primitiv has three tiers, and each one only ever points at the tier below it.

The three tiers

Palette — the raw colours.

Six scales of ten steps: neutral, brand, success, warning, danger and info. This is the only tier that holds an actual colour value. Harmoni generates it. A larger set of standard ramps is available too, for colour the semantic roles do not own — see below.

Intent — what a colour is for.

surface/default is the page background. content/primary is body text. border/subtle is a hairline. None of them holds a value. Each points at a Palette step. Light and dark are two modes of this tier, which is why switching theme is a mode swap rather than a second stylesheet.

Context — how big things are.

Control heights, padding, gaps, corner radius, type size. This tier has four modes, one per density setting. That is the whole density system.

Component

Context

framed-control/md/height
framed-control/md/padding-inline

Intent

  • action/primary/default
  • content/on-action

Palette brand

  1. 50
  2. 100
  3. 200
  4. 300
  5. 400
  6. 500
  7. 600
  8. 700
  9. 800
  10. 900
  11. white

oklch(0.5557 0.1923 259.8783)

Only the bottom tier holds a value. Everything above it points.

Why the layering matters

Because only the bottom tier holds a value, a rebrand is a change to one tier. Every role above it keeps pointing where it pointed, and the new colour arrives everywhere at once.

The same rule gives you dark mode. surface/default means "the page background" in both themes and resolves to a different palette step in each, so a theme is a set of values rather than a set of overrides.

Changing your tokens

To change the brand colour, regenerate the theme:

$ npx primitiv theme --brand "#0a7755"

That produces a full light and dark palette from your colour, with every semantic role reassigned by contrast. It writes into its own layer, so it beats the base tokens without you editing them. The standard ramps are fixed and are not touched by this.

You can seed the status ramps the same way, with --danger, --warning, --success and --info. Record them once in primitiv.json and later runs pick them up.

To change something the generator does not own, such as a spacing value or a font, override the custom property in your own stylesheet. Token names are the contract, and they do not change under you.

Ramp length

Ten steps per ramp suits most projects. If you want finer gradations, or a smaller set, ask for a different length:

$ npx primitiv theme --brand "#0a7755" --steps 16

Anything from 3 to 32 works. The semantic roles keep up: a sixteen-step ramp has no 600, so any role that pointed at one moves to the nearest step that exists, written into the same file.

The components do not keep up. Their stylesheets are written against ten steps, so at any other length the styling is yours to maintain. If you want a different length without that cost, 18 and 26 also carry every step the components ask for.

Tokens emit in three formats. Set it once in primitiv.json:

CSS
Custom properties. The default, and what the rest of the docs assume.
SCSS
The same values as SCSS variables.
Tailwind
A theme extension.

What is in the token layer

Colour is the tier most people meet first, but it is not the only one. The system also tokenises spacing, type, corner radius, shadows, motion, breakpoints and interaction states. All of it emits together.