@uncinq/design-tokens 1.7.3 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/dtcg.md ADDED
@@ -0,0 +1,232 @@
1
+ ---
2
+ isIndex: false
3
+ title: DTCG format
4
+ description: The Design Tokens Community Group JSON format, its keys, types and naming conventions as used in this package.
5
+ weight: 6
6
+ icon: braces
7
+ ---
8
+
9
+
10
+ The [W3C Design Token Community Group (DTCG)](https://www.w3.org/community/design-tokens/) defines a standard interchange format for design tokens, so they can travel between tools (Figma, code, documentation) without loss of meaning.
11
+
12
+ `@uncinq/design-tokens` uses DTCG JSON as its source format. [Style Dictionary v5](https://styledictionary.com/) transforms those JSON files into CSS custom properties — see [STYLE-DICTIONARY.md](STYLE-DICTIONARY.md) for the build pipeline. The DTCG spec informs the architecture (primitive → semantic → component, naming conventions, token types).
13
+
14
+ ---
15
+
16
+ ## The DTCG format
17
+
18
+ The [DTCG spec](https://tr.designtokens.org/format/) defines tokens as JSON objects with reserved `$`-prefixed keys:
19
+
20
+ ```json
21
+ {
22
+ "color": {
23
+ "brand": {
24
+ "$value": "oklch(0.530 0.195 22.0)",
25
+ "$type": "color",
26
+ "$description": "Primary brand color — used for CTAs and highlights."
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ > **Note on OKLCH:** The DTCG `color` type accepts any valid CSS color value, including `oklch(…)`. `@uncinq/design-tokens` uses OKLCH throughout — perceptually uniform, wide-gamut, and natively supported in modern browsers.
33
+
34
+ | Key | Required | Description |
35
+ | --- | --- | --- |
36
+ | `$value` | ✅ | The token's value |
37
+ | `$type` | recommended | The token type (see below) |
38
+ | `$description` | optional | Human-readable documentation |
39
+ | `$extensions` | optional | Vendor-specific metadata (e.g. Figma) |
40
+
41
+ ---
42
+
43
+ ## DTCG token types
44
+
45
+ ### Scalar types
46
+
47
+ | Type | Example value | CSS usage |
48
+ | --- | --- | --- |
49
+ | `color` | `oklch(0.530 0.195 22.0)` | `color`, `background-color` |
50
+ | `dimension` | `1rem`, `4px` | `width`, `padding`, `font-size` |
51
+ | `fontFamily` | `"system-ui, sans-serif"` | `font-family` |
52
+ | `fontWeight` | `700` | `font-weight` |
53
+ | `duration` | `300ms` | `transition-duration` |
54
+ | `cubicBezier` | `[0.165, 0.84, 0.44, 1]` | `animation-timing-function` |
55
+ | `number` | `1.5` | `line-height`, `opacity` |
56
+ | `string` | `"uppercase"` | free-form text values |
57
+ | `strokeStyle` | `"solid"`, `"dashed"` | `border-style` |
58
+
59
+ ### Composite types
60
+
61
+ | Type | Shape | CSS usage |
62
+ | --- | --- | --- |
63
+ | `shadow` | `{offsetX, offsetY, blur, spread, color}` | `box-shadow` |
64
+ | `border` | `{width, style, color}` | `border` shorthand |
65
+ | `transition` | `{duration, delay, timingFunction}` | `transition` shorthand |
66
+ | `typography` | `{fontFamily, fontSize, fontWeight, letterSpacing, lineHeight}` | typography rules |
67
+ | `gradient` | `{gradientType, stops[]}` | `background: linear-gradient(…)` |
68
+
69
+ → Full type list: [tr.designtokens.org/format/#types](https://tr.designtokens.org/format/#types)
70
+
71
+ > **Note on `clamp()` values:** DTCG has no native `fluid` type. Fluid tokens (`--font-size-fluid-sm`, `--font-size-fluid-md`, `--spacing-fluid-*`) use `$type: "dimension"` as the closest match — a documented gap in the spec.
72
+
73
+ ---
74
+
75
+ ## Naming conventions
76
+
77
+ ### CSS compound properties → camelCase
78
+
79
+ CSS property names that are two words (kebab-case in CSS) are written as a single **camelCase key** — not as a nested group. The `pathToKebab` transform converts them back to kebab-case for the CSS output, so the result is identical either way.
80
+
81
+ | JSON key | CSS custom property |
82
+ | --- | --- |
83
+ | `"fontFamily"` | `--btn-font-family` |
84
+ | `"fontSize"` | `--btn-font-size` |
85
+ | `"fontStyle"` | `--btn-font-style` |
86
+ | `"fontWeight"` | `--btn-font-weight` |
87
+ | `"lineHeight"` | `--btn-line-height` |
88
+ | `"maxHeight"` | `--btn-max-height` |
89
+ | `"maxWidth"` | `--btn-max-width` |
90
+ | `"textAlign"` | `--btn-text-align` |
91
+ | `"textDecoration"` | `--btn-text-decoration` |
92
+ | `"textTransform"` | `--btn-text-transform` |
93
+
94
+ ```json
95
+ // ✅ correct
96
+ "btn": {
97
+ "fontSize": { "$value": "{fontSize.sm}", "$type": "dimension" },
98
+ "fontWeight": { "$value": "{fontWeight.bold}", "$type": "fontWeight" }
99
+ }
100
+
101
+ // ❌ wrong
102
+ "btn": {
103
+ "font": {
104
+ "size": { "$value": "{fontSize.sm}", "$type": "dimension" },
105
+ "weight": { "$value": "{fontWeight.bold}", "$type": "fontWeight" }
106
+ }
107
+ }
108
+ ```
109
+
110
+ **Exception — semantic namespaces:** `border`, `color`, `padding`, `margin`, `shadow` used to group multiple sub-properties stay nested, because the group key itself is not a CSS property compound word.
111
+
112
+ ```json
113
+ // ✅ border as a namespace grouping multiple properties
114
+ "border": {
115
+ "radius": { "$value": "{radius.control}", "$type": "dimension" },
116
+ "width": { "$value": "{border.width.sm}", "$type": "dimension" }
117
+ }
118
+
119
+ // ✅ color as a semantic grouping
120
+ "color": {
121
+ "background": { "$value": "{color.background.default}", "$type": "color" },
122
+ "text": { "$value": "{color.text.default}", "$type": "color" }
123
+ }
124
+ ```
125
+
126
+ ### Logical properties → sub-keys of `padding` / `margin`
127
+
128
+ Sub-axes of `padding` and `margin` use **CSS logical property names** as sub-keys — not physical directions (`x`, `y`, `top`, `bottom`, `left`, `right`).
129
+
130
+ | Sub-key | CSS logical property | Physical equivalent |
131
+ | --- | --- | --- |
132
+ | `"inline"` | `padding-inline` / `margin-inline` | left + right |
133
+ | `"block"` | `padding-block` / `margin-block` | top + bottom |
134
+ | `"inlineStart"` | `padding-inline-start` / `margin-inline-start` | left (LTR) |
135
+ | `"inlineEnd"` | `padding-inline-end` / `margin-inline-end` | right (LTR) |
136
+ | `"blockStart"` | `padding-block-start` / `margin-block-start` | top |
137
+ | `"blockEnd"` | `padding-block-end` / `margin-block-end` | bottom |
138
+
139
+ ```json
140
+ // ✅ correct
141
+ "padding": {
142
+ "inline": { "$value": "{spacing.sm}", "$type": "dimension" },
143
+ "block": { "$value": "{spacing.xs}", "$type": "dimension" }
144
+ }
145
+
146
+ // ❌ wrong
147
+ "padding": {
148
+ "x": { "$value": "{spacing.sm}", "$type": "dimension" },
149
+ "y": { "$value": "{spacing.xs}", "$type": "dimension" }
150
+ }
151
+ ```
152
+
153
+ **Exception — CSS `top`/`bottom`/`left`/`right` as positioning values** (not margin/padding sub-keys) keep their physical names, since they map to CSS position properties, not logical shorthands.
154
+
155
+ ```json
156
+ // ✅ positioning — physical names are correct here
157
+ "sticky": {
158
+ "top": { "$value": "4rem", "$type": "dimension" }
159
+ }
160
+ ```
161
+
162
+ ### States → nested sub-keys
163
+
164
+ Interactive states (`default`, `hover`, `active`, `disabled`…) are expressed as **nested keys** under the property they modify. The `default` key is automatically stripped by the build transform.
165
+
166
+ ```json
167
+ "color": {
168
+ "background": {
169
+ "default": { "$value": "{color.brand.default}", "$type": "color" },
170
+ "hover": { "$value": "{color.brand.hover}", "$type": "color" }
171
+ }
172
+ }
173
+ ```
174
+
175
+ ```css
176
+ /* output */
177
+ --btn-color-background: var(--color-brand);
178
+ --btn-color-background-hover: var(--color-brand-hover);
179
+ ```
180
+
181
+ States and camelCase properties compose naturally:
182
+
183
+ ```json
184
+ "color": {
185
+ "textDecoration": {
186
+ "default": { "$value": "transparent", "$type": "color" },
187
+ "hover": { "$value": "{color.link.default}", "$type": "color" }
188
+ }
189
+ }
190
+ ```
191
+
192
+ ```css
193
+ --btn-color-text-decoration: transparent;
194
+ --btn-color-text-decoration-hover: var(--color-link);
195
+ ```
196
+
197
+ ---
198
+
199
+ ## References (aliases)
200
+
201
+ Tokens can reference other tokens using `{dotted.path}` syntax:
202
+
203
+ ```json
204
+ {
205
+ "color": {
206
+ "link": {
207
+ "default": {
208
+ "$value": "{color.brand.default}",
209
+ "$type": "color"
210
+ }
211
+ }
212
+ }
213
+ }
214
+ ```
215
+
216
+ In CSS, this maps to `var()`:
217
+
218
+ ```css
219
+ --color-link: var(--color-brand);
220
+ ```
221
+
222
+ This is the key mechanism behind the **primitive → semantic → component** hierarchy.
223
+
224
+ → [tr.designtokens.org/format/#alias](https://tr.designtokens.org/format/#alias)
225
+
226
+ ---
227
+
228
+ ## References
229
+
230
+ - [DTCG specification](https://tr.designtokens.org/format/) — W3C Community Group draft
231
+ - [DTCG GitHub](https://github.com/design-tokens/community-group) — issues, discussion
232
+ - [Style Dictionary v5](https://styledictionary.com/) — token build pipeline, see [STYLE-DICTIONARY.md](STYLE-DICTIONARY.md)
package/docs/naming.md ADDED
@@ -0,0 +1,128 @@
1
+ ---
2
+ isIndex: false
3
+ title: Naming
4
+ description: The naming grammar for semantic and component tokens, the rules that keep it consistent, and the scales in use.
5
+ weight: 1
6
+ icon: tag
7
+ ---
8
+
9
+ A token name is an API. Once a project references `--color-text-muted`, renaming it is a breaking change, so the grammar below is worth following closely.
10
+
11
+ ## Semantic tokens
12
+
13
+ ```
14
+ --{category}-{subcategory?}-{variant?}-{state?}
15
+ ```
16
+
17
+ | Pattern | Example |
18
+ | --- | --- |
19
+ | `--{category}` | `--color` |
20
+ | `--{category}-{subcategory}` | `--color-text` |
21
+ | `--{category}-{subcategory}-{variant}` | `--color-text-muted` |
22
+ | `--{category}-{subcategory}-{state}` | `--color-text-disabled` |
23
+
24
+ ## Component tokens
25
+
26
+ ```
27
+ --{component}-{property}-{sub-property?}-{state?}
28
+ ```
29
+
30
+ The property mirrors the CSS property name, so the token reads the same way as the declaration it controls. Colors are the exception: `color` leads and the role follows, for the reason given in the rules below.
31
+
32
+ | Pattern | Example |
33
+ | --- | --- |
34
+ | `--{component}` | `--btn` |
35
+ | `--{component}-{property}` | `--btn-padding-inline` |
36
+ | `--{component}-{property}-{sub-property}` | `--btn-color-text-decoration` |
37
+ | `--{component}-{property}-{state}` | `--btn-color-background-hover` |
38
+
39
+ ## Rules
40
+
41
+ **Lowercase kebab-case**, always.
42
+
43
+ **No component names in primitive or semantic tokens.** `--button-*` belongs in [@uncinq/component-tokens](https://github.com/uncinq/component-tokens), not here. A semantic token that names a component has stopped being semantic.
44
+
45
+ **Semantic tokens are named by intent.** They usually reference a primitive through `var()`, but they may carry a raw value when the value itself is the design decision, such as `--z-index-modal: 400` or `--radius-pill: 9999px`.
46
+
47
+ **`color-[role]` for every color token.** `color` is the category prefix and the UI role follows: `color-background`, `color-border`, `color-text`, `color-accent`, `color-placeholder`. This groups all color tokens alphabetically under `color-*`, and it makes the component token mirror the global one, `--color-background` becoming `--btn-color-background`. `background` is never abbreviated, so `color-background` and never `color-bg`.
48
+
49
+ | Token | Role | CSS property it drives |
50
+ | --- | --- | --- |
51
+ | `--btn-color-background` | background | `background-color` |
52
+ | `--btn-color-border` | border | `border-color` |
53
+ | `--btn-color-text` | text | `color` |
54
+ | `--btn-color-text-decoration` | text-decoration | `text-decoration-color` |
55
+ | `--form-color-accent` | accent | `color` |
56
+ | `--input-color-placeholder` | placeholder | `color` |
57
+
58
+ **States go last**: `-hover`, `-focus`, `-active`, `-disabled`, `-checked`.
59
+
60
+ **Alphabetical order within a file**, grouped with a comment once a file has many entries.
61
+
62
+ ```css
63
+ /* Brand */
64
+ --color-brand: var(--color-sienna-600);
65
+ --color-brand-hover: var(--color-sienna-700);
66
+
67
+ /* Text */
68
+ --color-text: var(--color-gray-900);
69
+ --color-text-muted: var(--color-gray-500);
70
+ ```
71
+
72
+ ## The `default` convention
73
+
74
+ In the JSON source, a state lives in a nested key and `default` is the unstated one. The build strips it from the generated name, so `color.brand.default` becomes `--color-brand` while `color.brand.hover` becomes `--color-brand-hover`.
75
+
76
+ ```json
77
+ "brand": {
78
+ "default": { "$value": "{color.sienna.600}", "$type": "color" },
79
+ "hover": { "$value": "{color.sienna.700}", "$type": "color" }
80
+ }
81
+ ```
82
+
83
+ This is what lets `--color-brand` and `--color-brand-hover` sit in the same group without one of them being named `--color-brand-default`. See [DTCG format](../dtcg/) for the full set of authoring conventions.
84
+
85
+ ## Scales
86
+
87
+ | Use case | Scale | Example |
88
+ | --- | --- | --- |
89
+ | Color palettes | Numeric, 50 to 950 | `--color-gray-500` |
90
+ | Heading levels | Zero-padded, 01 to 06 | `--font-size-heading-01` |
91
+ | Layout and spacing | T-shirt, `2xs xs sm md lg xl 2xl` | `--spacing-md` |
92
+ | Radius, shadow, size | T-shirt | `--radius-sm` |
93
+ | Purposeful aliases | Named | `--radius-control`, `--radius-pill` |
94
+
95
+ The zero-padding on heading levels is there so that `01` through `06` sort correctly as text, which matters because the generated CSS is ordered alphabetically.
96
+
97
+ ## Category reference
98
+
99
+ | Category | Covers | Example |
100
+ | --- | --- | --- |
101
+ | `color` | All color values | `--color-brand`, `--color-text-on-dark` |
102
+ | `font-family` | Typefaces | `--font-family-heading` |
103
+ | `font-size` | Text sizes | `--font-size-sm`, `--font-size-heading-01` |
104
+ | `font-size-fluid` | Responsive fluid type scale | `--font-size-fluid-xl` |
105
+ | `font-weight` | Weight values | `--font-weight-bold` |
106
+ | `line-height` | Line heights | `--line-height-heading` |
107
+ | `letter-spacing` | Tracking | `--letter-spacing-md` |
108
+ | `text-decoration` | Decoration properties | `--text-decoration-offset` |
109
+ | `spacing` | Margin and padding | `--spacing-md`, `--spacing-section` |
110
+ | `spacing-fluid` | Responsive fluid spacing scale | `--spacing-fluid-lg` |
111
+ | `size` | Width and height | `--size-16`, `--size-tablet` |
112
+ | `max-width` | Readability caps | `--max-width-paragraph` |
113
+ | `radius` | Border radius | `--radius-md`, `--radius-pill` |
114
+ | `border` | Border style and width | `--border-width-sm` |
115
+ | `shadow` | Box shadows | `--shadow-md` |
116
+ | `blur` | Blur values | `--blur-md` |
117
+ | `gradient` | Overlay gradients | `--gradient-darken-color-from` |
118
+ | `opacity` | Opacity values | `--opacity-disabled` |
119
+ | `duration` | Animation timing | `--duration-fast` |
120
+ | `easing` | Timing functions | `--easing-out-expo` |
121
+ | `transition` | Shorthand transitions | `--transition-normal` |
122
+ | `ratio` | Aspect ratios | `--ratio-video` |
123
+ | `focus` | Focus ring tokens | `--focus-outline-width` |
124
+ | `grid` | Column counts, gaps, fractions | `--columns-tablet`, `--gap` |
125
+ | `icon` | SVG icons as data URIs | `--icon-arrow` |
126
+ | `z-index` | Stacking order | `--z-index-modal` |
127
+
128
+ For the actual values behind each of these, see the [Reference](../reference/), which is generated from the JSON sources and cannot drift.
@@ -0,0 +1,129 @@
1
+ ---
2
+ isIndex: false
3
+ title: Reference
4
+ description: Every token in the package, generated from the JSON sources so it can never drift from the shipped CSS.
5
+ weight: 5
6
+ icon: table
7
+ ---
8
+
9
+ Every table below is generated at build time from `dist/tokens.json`, which Style Dictionary produces from the same JSON sources as the CSS. Nothing here is written by hand, so a token cannot appear in the reference without existing in the package, or change value without the page changing with it.
10
+
11
+ Values are shown as they are emitted. A semantic token that references a primitive shows `var(--the-primitive)` rather than a flattened value, because that is literally what ships, and it is what makes an override of the primitive propagate.
12
+
13
+ {{< alert-block state="info" >}}
14
+ Reading this offline, from the repository or from `node_modules`? The tables below are rendered by the documentation site. The same data, in machine-readable form, sits in `dist/tokens.json`, and the final CSS is in `dist/css/`.
15
+ {{< /alert-block >}}
16
+
17
+ ## Primitive
18
+
19
+ Raw, context-free values. These answer "what is the value?" and carry no opinion about usage.
20
+
21
+ ### Color palette
22
+
23
+ 19 hues at 11 steps, plus black and white. See [Colors](../colors/) for the hue table and the step guide.
24
+
25
+ {{< tokens pkg="design" file="primitive/color" >}}
26
+
27
+ ### Size scale
28
+
29
+ {{< tokens pkg="design" file="primitive/size" >}}
30
+
31
+ ### Font primitives
32
+
33
+ {{< tokens pkg="design" file="primitive/font" >}}
34
+
35
+ ### Shadow scale
36
+
37
+ {{< tokens pkg="design" file="primitive/shadow" >}}
38
+
39
+ ### Blur scale
40
+
41
+ {{< tokens pkg="design" file="primitive/blur" >}}
42
+
43
+ ## Semantic
44
+
45
+ Named by purpose. These answer "what is this value for?" and are the layer a project should override.
46
+
47
+ ### Color roles
48
+
49
+ {{< tokens pkg="design" file="semantic/color" >}}
50
+
51
+ ### Typography
52
+
53
+ Font sizes (fixed and fluid), heading sizes, line heights, letter spacing, and the `--max-width-*` readability caps, which are authored as a `maxWidth` group in this file.
54
+
55
+ {{< tokens pkg="design" file="semantic/typography" >}}
56
+
57
+ ### Spacing
58
+
59
+ {{< tokens pkg="design" file="semantic/spacing" >}}
60
+
61
+ ### Size aliases
62
+
63
+ {{< tokens pkg="design" file="semantic/size" >}}
64
+
65
+ ### Grid
66
+
67
+ {{< tokens pkg="design" file="semantic/grid" >}}
68
+
69
+ ### Radius
70
+
71
+ {{< tokens pkg="design" file="semantic/radius" >}}
72
+
73
+ ### Border
74
+
75
+ {{< tokens pkg="design" file="semantic/border" >}}
76
+
77
+ ### Shadow aliases
78
+
79
+ {{< tokens pkg="design" file="semantic/shadow" >}}
80
+
81
+ ### Blur aliases
82
+
83
+ {{< tokens pkg="design" file="semantic/blur" >}}
84
+
85
+ ### Gradient
86
+
87
+ {{< tokens pkg="design" file="semantic/gradient" >}}
88
+
89
+ ### Opacity
90
+
91
+ {{< tokens pkg="design" file="semantic/opacity" >}}
92
+
93
+ ### Motion
94
+
95
+ Durations, easing curves and the transition shorthands built from them.
96
+
97
+ {{< tokens pkg="design" file="semantic/motion" >}}
98
+
99
+ ### Ratio
100
+
101
+ {{< tokens pkg="design" file="semantic/ratio" >}}
102
+
103
+ ### Focus
104
+
105
+ {{< tokens pkg="design" file="semantic/focus" >}}
106
+
107
+ ### Form
108
+
109
+ The largest semantic file. It covers inputs, labels, checkboxes, radios, switches, selects and textareas, and is what [@uncinq/css-base](../../css-base/base/) reads to style native form controls.
110
+
111
+ {{< tokens pkg="design" file="semantic/form" >}}
112
+
113
+ ### Icon
114
+
115
+ SVG icons encoded as `url('data:image/svg+xml;utf8,...')`, usable as a `background-image` or a `mask-image`.
116
+
117
+ {{< tokens pkg="design" file="semantic/icon" >}}
118
+
119
+ ### Z-index
120
+
121
+ {{< tokens pkg="design" file="semantic/z-index" >}}
122
+
123
+ ## Themes
124
+
125
+ ### Dark
126
+
127
+ The overlay applied under `prefers-color-scheme: dark`. See [Dark mode](../dark-mode/) for how the selector works and how to opt out.
128
+
129
+ {{< tokens pkg="design" file="themes/dark" >}}
@@ -0,0 +1,182 @@
1
+ ---
2
+ isIndex: false
3
+ title: Style Dictionary
4
+ description: How the JSON token sources are compiled into CSS custom properties, including the dark theme pass.
5
+ weight: 7
6
+ icon: gear
7
+ ---
8
+
9
+
10
+ [Style Dictionary v5](https://styledictionary.com/) transforms the DTCG JSON token files into CSS custom properties.
11
+
12
+ ## Run the build
13
+
14
+ ```bash
15
+ npm run build
16
+ ```
17
+
18
+ Output is written to `dist/css/`. One CSS file is generated per JSON source file, preserving the same directory structure:
19
+
20
+ ```
21
+ tokens/primitive/color.json → dist/css/primitive/color.css
22
+ tokens/semantic/color.json → dist/css/semantic/color.css
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Config — `style-dictionary.config.js`
28
+
29
+ The config is a single ES module. It registers a custom name transform and a custom format, then maps every JSON file to a CSS output.
30
+
31
+ ### Name transform — `name/kebab/strip-default`
32
+
33
+ Converts the token path to a kebab-case CSS custom property name, dropping any `default` segment:
34
+
35
+ ```
36
+ color.background.default → --color-background
37
+ color.text.muted → --color-text-muted
38
+ ```
39
+
40
+ ### Format — `css/layer-tokens`
41
+
42
+ All tokens are wrapped in `@layer tokens { :root { … } }`. This is a low-priority layer in the Un Cinq stack (order: `reset, tokens, base, layouts, vendors, components`), so any project can override any token by importing after this package inside its own `@layer tokens` block.
43
+
44
+ References are preserved as `var()` — tokens are **not** resolved to their final values:
45
+
46
+ ```css
47
+ /* output */
48
+ --color-text: var(--color-gray-900); /* not oklch(0.208 0.006 264.542) */
49
+ ```
50
+
51
+ This is done by reading `token.original.$value` and replacing every `{path.to.token}` reference with `var(--path-to-token)`. References embedded in a string value are also handled:
52
+
53
+ ```json
54
+ { "$value": "{size.8} {size.12}" }
55
+ ```
56
+
57
+ ```css
58
+ --file-button-padding: var(--size-8) var(--size-12);
59
+ ```
60
+
61
+ ### Composite tokens
62
+
63
+ Tokens whose `$value` is an object or an array of objects (e.g. `shadow`) are serialized as a space-separated CSS value. References inside the composite are converted to `var()`.
64
+
65
+ **Property order matters** — list properties in the order CSS expects them. For `box-shadow`:
66
+
67
+ ```json
68
+ {
69
+ "shadow": {
70
+ "sm": {
71
+ "$type": "shadow",
72
+ "$value": [
73
+ {
74
+ "offsetX": "0",
75
+ "offsetY": "1px",
76
+ "blur": "2px",
77
+ "spread": "0",
78
+ "color": "{color.shadow.light}",
79
+ "inset": false
80
+ }
81
+ ]
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ ```css
88
+ --shadow-sm: 0 1px 2px 0 var(--color-shadow-light);
89
+ ```
90
+
91
+ `inset` is always treated as a boolean prefix (`inset` or nothing), not a positional value.
92
+
93
+ ---
94
+
95
+ ## Dark mode — `themes/dark.json`
96
+
97
+ Dark mode is a **separate token set**, `tokens/themes/dark.json` — **not** declared inline on tokens (there is no `$mods`/`$modes` extension).
98
+
99
+ The model is **default + override**:
100
+
101
+ - The `semantic` layer carries the **default (light)** values, at their natural place (`semantic/color.json`, `semantic/form.json`). There is **no** `themes/light.json` — light is the baseline, not a mode you switch into.
102
+ - `themes/dark.json` holds **only the tokens that differ** in dark mode (background, text, heading, shadow, border, form background). Everything mode-invariant (brand, accent, status, …) is inherited automatically through `var()`.
103
+
104
+ This mirrors the DTCG **Resolver** philosophy (a base set + an overlay holding only the differences), so it stays forward-compatible as the Resolver spec stabilises.
105
+
106
+ ### Generated output
107
+
108
+ The build emits `dist/css/themes/dark.css` with a single rule, inside `@layer tokens`:
109
+
110
+ ```css
111
+ @layer tokens {
112
+ @media (prefers-color-scheme: dark) {
113
+ :root:not([data-color-scheme="light"]) {
114
+ --color-background: var(--color-gray-950);
115
+ /* … */
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ The light defaults stay in `dist/css/semantic/*.css` on `:root`. The dark file is purely additive — a light-only (mono-mode) site never needs to load it.
122
+
123
+ ### Activation matrix
124
+
125
+ Dark mode follows the OS preference. The only attribute that does anything is `data-color-scheme="light"`, which **forces light** even when the OS prefers dark. Forcing dark against a light OS is intentionally **not** supported (there is no `[data-color-scheme="dark"]` rule).
126
+
127
+ | `data-color-scheme` on `<html>` | OS preference | Result | Why |
128
+ | --- | --- | --- | --- |
129
+ | *(none)* | light | **light** | `:root` defaults; the media query is inactive |
130
+ | *(none)* | dark | **dark** | the media rule matches `:root` (not forced light) |
131
+ | `light` | dark | **light** | `:not([data-color-scheme="light"])` excludes it → `:root` defaults |
132
+ | `light` | light | **light** | `:root` defaults; the media query is inactive |
133
+ | *(any other)* | dark | **dark** | the media rule still matches — only `light` opts out |
134
+
135
+ > **Note** — `color-scheme: dark` is intentionally absent from the generated block. The `color-scheme` property is managed at the HTML level by the consuming application (e.g. via `<meta name="color-scheme">`), not by the token layer.
136
+
137
+ ### Rules
138
+
139
+ - Only colour tokens that actually change between modes belong in `themes/dark.json`; everything mode-invariant stays in `semantic`.
140
+ - Dark values follow the same `{dotted.path}` reference syntax as `$value` (e.g. `{color.gray.950}`).
141
+
142
+ ---
143
+
144
+ ## Adding a new token file
145
+
146
+ 1. Create a JSON file anywhere under `tokens/` with DTCG structure.
147
+ 2. Run `npm run build` — the file is detected automatically.
148
+ 3. A matching CSS file is generated in `dist/css/`.
149
+
150
+ No changes to `style-dictionary.config.js` are needed.
151
+
152
+ ---
153
+
154
+ ## References
155
+
156
+ - [Style Dictionary v5 docs](https://styledictionary.com/)
157
+ - [DTCG format](DTCG.md) — token structure and types
158
+ - [Utopia fluid scales](UTOPIA.md) — `clamp()` values in spacing and typography tokens
159
+
160
+ ---
161
+
162
+ ## The JSON manifest
163
+
164
+ Alongside the CSS, the build writes `dist/tokens.json`: a flat array of every token the package ships.
165
+
166
+ ```json
167
+ [
168
+ {
169
+ "name": "--color-brand",
170
+ "value": "var(--color-sienna-600)",
171
+ "type": "color",
172
+ "file": "semantic/color",
173
+ "description": ""
174
+ }
175
+ ]
176
+ ```
177
+
178
+ The documentation site renders its reference tables from this file, which is why the reference cannot drift from the stylesheets.
179
+
180
+ The guarantee comes from a single shared function. `tokenToCssValue()` serializes a token to its CSS value, and **both** the CSS format and the manifest format call it. There is no second implementation of the naming or the value logic to fall out of step, and the build asserts the equivalence: the manifest holds exactly one entry per declaration emitted in `dist/css/`, with the same name and the same value.
181
+
182
+ `description` comes from the DTCG `$description` key. Adding one to a token source makes it appear in the published reference with no other change.