Skip to main content

Design Tokens (DTCG)

A Theme in Shio carries a TOKENS field. It holds design tokens in the W3C DTCG format, and Shio compiles them into CSS custom properties on every page the theme renders.

The reason it exists is narrow and worth stating first, because it explains every decision below.


The problem tokens solve

Before this, a Theme held CSS and JAVASCRIPT as opaque blobs. So "make the accent colour warmer", "tighten the headings" and "more density", which are the commonest changes anybody asks for, were all stylesheet rewrites. That has three costs:

  • Nothing declares what may be changed. A stylesheet does not say which values are design decisions and which are incidental.
  • Nothing verifies the result. A diff shows a changed string.
  • An agent editing raw CSS by string replacement is the least reliable thing in the loop. It is the operation most likely to produce a page that renders and looks wrong.

A declared token vocabulary turns a restyle into a field edit: reviewable in the console, diffable in the file projection, and safe for an agent because the set of names is closed.

Raw CSS stays available. It is the escape hatch, not the interface.


What DTCG is

DTCG is the format published by the W3C Design Tokens Community Group. A token file is JSON: nested groups, and a token is any node carrying a $value.

{
"color": {
"accent": { "$value": "#0b5d3b", "$type": "color" },
"text": { "$value": "#1a1a1a", "$type": "color" },
"background": { "$value": "#ffffff", "$type": "color" }
},
"font": {
"family": { "body": { "$value": "Inter, system-ui, sans-serif", "$type": "fontFamily" } },
"size": { "base": { "$value": "1rem", "$type": "dimension" } }
},
"radius": {
"card": { "$value": "12px", "$type": "dimension", "$description": "Cards and panels" }
}
}

Three parts of the spec matter here:

KeyMeans
$valueThe token's value. A node that has one is a token, and the walk stops there
$typeWhat kind of value it is (color, dimension, fontFamily, fontWeight, shadow, …)
$descriptionProse for a reader. Carried, never rendered

The $ prefix is the spec's, not a Shio convention. Any other $-prefixed key at group level is spec metadata and is skipped.

Shio adopted this format rather than inventing one. That was the open question when the feature was designed, and the answer is that a dialect nothing else reads is a design project with no payoff: Style Dictionary, Figma and Tokens Studio already export and import DTCG, so a designer's export pastes straight into the field.


What Shio does with it

Compiled to custom properties

Every token becomes a CSS custom property on :root, with the dotted path as the name:

Token pathCustom property
color.accent--color-accent
font.family.body--font-family-body
radius.card--radius-card

The generated block is prepended to the theme's own CSS:

:root{--color-accent:#0b5d3b;--color-text:#1a1a1a;--font-family-body:Inter, system-ui, sans-serif;--radius-card:12px;}

.card {
background: var(--color-background);
border-radius: var(--radius-card);
}

Prepending is deliberate. The alternative was a separate {{theme.tokens.css}} handle for layouts to place, and that would ship the feature switched off: every existing layout would render without tokens until somebody edited a template most authors do not know to change. Prepending also puts the declarations before the rules that use them, which is the order CSS needs anyway.

Nothing new has to learn anything: every stylesheet, every browser and every devtool already reads custom properties.

Readable from a template

The same values are exposed as a flattened, dotted map:

<svg viewBox="0 0 24 24"><path fill="{{theme.tokens.color.accent}}" d="…"/></svg>
<td style="background:{{theme.tokens.color.background}}">…</td>

This exists for the places var() cannot reach: an SVG fill, an email template, an inline style consumed by something that does not resolve custom properties. Same value, two forms, one source.


Authoring them

TOKENS is a field on the Theme post, so it is edited like any other content:

PathHow
ConsoleOpen the Theme, edit the Design tokens field (a code editor, since the field is JSON)
FilesThe theme projects to disk; TOKENS travels in the post's data
An agentpost.upsert on the Theme's address, with data.TOKENS
MCPshio_write, same op
Replicationshio propose writes them from a captured stylesheet (below)

An agent example:

{
"ops": [
{ "op": "post.upsert", "address": "post:mysite/themes/main", "type": "Theme",
"data": { "TOKENS": "{\"color\":{\"accent\":{\"$value\":\"#0b5d3b\"}}}" } }
]
}
An older instance may not have the field

TOKENS is the render model's one deliberate addition beyond the original Theme shape, so an instance created before it exists will not have the field. GET /api/v2/render/post-types/drift names what is missing and POST /api/v2/render/post-types/reconcile adds it. An imported theme with no tokens keeps rendering exactly as it did.


The rules, and why each one is what it is

SituationWhat happensWhy
No tokens at allNothing is emitted, not even an empty :root{}A theme with no tokens must render byte-identically to how it rendered before the feature existed, or every render digest in the instance moves for something nobody used
A token whose $value is an object (a DTCG composite type)That token is skipped; its siblings still compileA composite has no single CSS form. Guessing one is worse than omitting it
A name that cannot be a custom property (font size)Rewritten to --font-sizeA custom property name cannot be quoted, so the browser drops the whole declaration silently, and in some parsers takes the rest of the block with it
A malformed valueSkipped, never a refusalA token file is content. Refusing to render a page over one typo would take the site down; shio verify is where a rule about it belongs
TOKENS is not valid JSONThe page renders on the theme's CSS alone, and a content incident is recordedThe page must not fail, and the degradation must not be silent. Read it at GET /api/v2/agent/diagnostics
Nesting deeper than 8 levelsIgnored below that depthA guard against a hand-written file with a cycle-shaped mistake

The shape of those decisions is consistent: a broken token costs you the token, a broken file costs you the tokens, and neither costs you the page.


Tokens from a site you are replicating

Design tokens are also how replication carries a captured site's styling. shio propose reads the captured stylesheet and offers a DTCG token set beside the block candidates, and shio convert writes it into Theme.TOKENS.

It reads two kinds of token, and they are not equally useful:

  • Declared custom properties are the strong case. If the source's own CSS already says var(--brand-primary), lifting that into TOKENS makes it genuinely editable: changing the token changes the replica. A modern site usually hands its whole palette over this way.
  • Recurring literals are the weaker case. A colour repeated eleven times is probably a design decision, but the source's CSS does not say so, so the token is a name for a value rather than a control.

The report distinguishes them, because a reader deciding whether the replica is editable needs to know which kind they got.

What it groups, and the DTCG $type it claims:

Group$typeRead from
color.text · color.background · color.bordercolorColour properties, grouped by the role the property implies
font.sizedimensionfont-size
font.familyfontFamilyfont-family
font.weightfontWeightfont-weight
spacingdimensionmargins, paddings, gaps
radiusdimensionthe border-radius family
border.widthdimensionthe border-width family
shadowshadowbox-shadow, text-shadow

A $type is claimed only when the reader can prove it from the value (a colour, a length). Otherwise it is left off rather than guessed.

Two details that came out of running this against real sites:

  • A declaration lifted into TOKENS is removed from the kept CSS. Otherwise the prepended block and the surviving rule would both set the value, and editing the token would change nothing.
  • A colliding name takes its whole name as one flat segment. DTCG forbids a group and a token sharing a name, so --bg and --bg-alt cannot become bg and bg.alt. They become bg and bg-alt. Splitting on every hyphen silently dropped seven of sixteen tokens on one real palette before this rule existed.

When not to use tokens

Tokens are for values a person would call a design decision. They are not a general CSS abstraction:

  • Layout structure (a grid definition, a media query) belongs in the theme's CSS.
  • A one-off value used once gains nothing from a name.
  • Anything with no single CSS value (a composite shadow across several properties, an animation) is skipped by design; write it in CSS.

The test is whether somebody would ask you to change it. If they would, it is a token.


PageDescription
Pages, Layouts & RegionsThe Theme post type and how its CSS reaches a page
ReplicationWhere a captured site's tokens come from
Content ModelingThe Theme post type's fields
The Agent Surfacepost.upsert, and the diagnostics a malformed file lands in