@uncinq/design-tokens 1.7.3 → 1.8.0

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,231 @@
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
+ ---
7
+
8
+
9
+ 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.
10
+
11
+ `@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).
12
+
13
+ ---
14
+
15
+ ## The DTCG format
16
+
17
+ The [DTCG spec](https://tr.designtokens.org/format/) defines tokens as JSON objects with reserved `$`-prefixed keys:
18
+
19
+ ```json
20
+ {
21
+ "color": {
22
+ "brand": {
23
+ "$value": "oklch(0.530 0.195 22.0)",
24
+ "$type": "color",
25
+ "$description": "Primary brand color — used for CTAs and highlights."
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ > **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.
32
+
33
+ | Key | Required | Description |
34
+ | --- | --- | --- |
35
+ | `$value` | ✅ | The token's value |
36
+ | `$type` | recommended | The token type (see below) |
37
+ | `$description` | optional | Human-readable documentation |
38
+ | `$extensions` | optional | Vendor-specific metadata (e.g. Figma) |
39
+
40
+ ---
41
+
42
+ ## DTCG token types
43
+
44
+ ### Scalar types
45
+
46
+ | Type | Example value | CSS usage |
47
+ | --- | --- | --- |
48
+ | `color` | `oklch(0.530 0.195 22.0)` | `color`, `background-color` |
49
+ | `dimension` | `1rem`, `4px` | `width`, `padding`, `font-size` |
50
+ | `fontFamily` | `"system-ui, sans-serif"` | `font-family` |
51
+ | `fontWeight` | `700` | `font-weight` |
52
+ | `duration` | `300ms` | `transition-duration` |
53
+ | `cubicBezier` | `[0.165, 0.84, 0.44, 1]` | `animation-timing-function` |
54
+ | `number` | `1.5` | `line-height`, `opacity` |
55
+ | `string` | `"uppercase"` | free-form text values |
56
+ | `strokeStyle` | `"solid"`, `"dashed"` | `border-style` |
57
+
58
+ ### Composite types
59
+
60
+ | Type | Shape | CSS usage |
61
+ | --- | --- | --- |
62
+ | `shadow` | `{offsetX, offsetY, blur, spread, color}` | `box-shadow` |
63
+ | `border` | `{width, style, color}` | `border` shorthand |
64
+ | `transition` | `{duration, delay, timingFunction}` | `transition` shorthand |
65
+ | `typography` | `{fontFamily, fontSize, fontWeight, letterSpacing, lineHeight}` | typography rules |
66
+ | `gradient` | `{gradientType, stops[]}` | `background: linear-gradient(…)` |
67
+
68
+ → Full type list: [tr.designtokens.org/format/#types](https://tr.designtokens.org/format/#types)
69
+
70
+ > **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.
71
+
72
+ ---
73
+
74
+ ## Naming conventions
75
+
76
+ ### CSS compound properties → camelCase
77
+
78
+ 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.
79
+
80
+ | JSON key | CSS custom property |
81
+ | --- | --- |
82
+ | `"fontFamily"` | `--btn-font-family` |
83
+ | `"fontSize"` | `--btn-font-size` |
84
+ | `"fontStyle"` | `--btn-font-style` |
85
+ | `"fontWeight"` | `--btn-font-weight` |
86
+ | `"lineHeight"` | `--btn-line-height` |
87
+ | `"maxHeight"` | `--btn-max-height` |
88
+ | `"maxWidth"` | `--btn-max-width` |
89
+ | `"textAlign"` | `--btn-text-align` |
90
+ | `"textDecoration"` | `--btn-text-decoration` |
91
+ | `"textTransform"` | `--btn-text-transform` |
92
+
93
+ ```json
94
+ // ✅ correct
95
+ "btn": {
96
+ "fontSize": { "$value": "{fontSize.sm}", "$type": "dimension" },
97
+ "fontWeight": { "$value": "{fontWeight.bold}", "$type": "fontWeight" }
98
+ }
99
+
100
+ // ❌ wrong
101
+ "btn": {
102
+ "font": {
103
+ "size": { "$value": "{fontSize.sm}", "$type": "dimension" },
104
+ "weight": { "$value": "{fontWeight.bold}", "$type": "fontWeight" }
105
+ }
106
+ }
107
+ ```
108
+
109
+ **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.
110
+
111
+ ```json
112
+ // ✅ border as a namespace grouping multiple properties
113
+ "border": {
114
+ "radius": { "$value": "{radius.control}", "$type": "dimension" },
115
+ "width": { "$value": "{border.width.sm}", "$type": "dimension" }
116
+ }
117
+
118
+ // ✅ color as a semantic grouping
119
+ "color": {
120
+ "background": { "$value": "{color.background.default}", "$type": "color" },
121
+ "text": { "$value": "{color.text.default}", "$type": "color" }
122
+ }
123
+ ```
124
+
125
+ ### Logical properties → sub-keys of `padding` / `margin`
126
+
127
+ Sub-axes of `padding` and `margin` use **CSS logical property names** as sub-keys — not physical directions (`x`, `y`, `top`, `bottom`, `left`, `right`).
128
+
129
+ | Sub-key | CSS logical property | Physical equivalent |
130
+ | --- | --- | --- |
131
+ | `"inline"` | `padding-inline` / `margin-inline` | left + right |
132
+ | `"block"` | `padding-block` / `margin-block` | top + bottom |
133
+ | `"inlineStart"` | `padding-inline-start` / `margin-inline-start` | left (LTR) |
134
+ | `"inlineEnd"` | `padding-inline-end` / `margin-inline-end` | right (LTR) |
135
+ | `"blockStart"` | `padding-block-start` / `margin-block-start` | top |
136
+ | `"blockEnd"` | `padding-block-end` / `margin-block-end` | bottom |
137
+
138
+ ```json
139
+ // ✅ correct
140
+ "padding": {
141
+ "inline": { "$value": "{spacing.sm}", "$type": "dimension" },
142
+ "block": { "$value": "{spacing.xs}", "$type": "dimension" }
143
+ }
144
+
145
+ // ❌ wrong
146
+ "padding": {
147
+ "x": { "$value": "{spacing.sm}", "$type": "dimension" },
148
+ "y": { "$value": "{spacing.xs}", "$type": "dimension" }
149
+ }
150
+ ```
151
+
152
+ **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.
153
+
154
+ ```json
155
+ // ✅ positioning — physical names are correct here
156
+ "sticky": {
157
+ "top": { "$value": "4rem", "$type": "dimension" }
158
+ }
159
+ ```
160
+
161
+ ### States → nested sub-keys
162
+
163
+ 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.
164
+
165
+ ```json
166
+ "color": {
167
+ "background": {
168
+ "default": { "$value": "{color.brand.default}", "$type": "color" },
169
+ "hover": { "$value": "{color.brand.hover}", "$type": "color" }
170
+ }
171
+ }
172
+ ```
173
+
174
+ ```css
175
+ /* output */
176
+ --btn-color-background: var(--color-brand);
177
+ --btn-color-background-hover: var(--color-brand-hover);
178
+ ```
179
+
180
+ States and camelCase properties compose naturally:
181
+
182
+ ```json
183
+ "color": {
184
+ "textDecoration": {
185
+ "default": { "$value": "transparent", "$type": "color" },
186
+ "hover": { "$value": "{color.link.default}", "$type": "color" }
187
+ }
188
+ }
189
+ ```
190
+
191
+ ```css
192
+ --btn-color-text-decoration: transparent;
193
+ --btn-color-text-decoration-hover: var(--color-link);
194
+ ```
195
+
196
+ ---
197
+
198
+ ## References (aliases)
199
+
200
+ Tokens can reference other tokens using `{dotted.path}` syntax:
201
+
202
+ ```json
203
+ {
204
+ "color": {
205
+ "link": {
206
+ "default": {
207
+ "$value": "{color.brand.default}",
208
+ "$type": "color"
209
+ }
210
+ }
211
+ }
212
+ }
213
+ ```
214
+
215
+ In CSS, this maps to `var()`:
216
+
217
+ ```css
218
+ --color-link: var(--color-brand);
219
+ ```
220
+
221
+ This is the key mechanism behind the **primitive → semantic → component** hierarchy.
222
+
223
+ → [tr.designtokens.org/format/#alias](https://tr.designtokens.org/format/#alias)
224
+
225
+ ---
226
+
227
+ ## References
228
+
229
+ - [DTCG specification](https://tr.designtokens.org/format/) — W3C Community Group draft
230
+ - [DTCG GitHub](https://github.com/design-tokens/community-group) — issues, discussion
231
+ - [Style Dictionary v5](https://styledictionary.com/) — token build pipeline, see [STYLE-DICTIONARY.md](STYLE-DICTIONARY.md)
package/docs/naming.md ADDED
@@ -0,0 +1,127 @@
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
+ ---
7
+
8
+ 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.
9
+
10
+ ## Semantic tokens
11
+
12
+ ```
13
+ --{category}-{subcategory?}-{variant?}-{state?}
14
+ ```
15
+
16
+ | Pattern | Example |
17
+ | --- | --- |
18
+ | `--{category}` | `--color` |
19
+ | `--{category}-{subcategory}` | `--color-text` |
20
+ | `--{category}-{subcategory}-{variant}` | `--color-text-muted` |
21
+ | `--{category}-{subcategory}-{state}` | `--color-text-disabled` |
22
+
23
+ ## Component tokens
24
+
25
+ ```
26
+ --{component}-{property}-{sub-property?}-{state?}
27
+ ```
28
+
29
+ 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.
30
+
31
+ | Pattern | Example |
32
+ | --- | --- |
33
+ | `--{component}` | `--btn` |
34
+ | `--{component}-{property}` | `--btn-padding-inline` |
35
+ | `--{component}-{property}-{sub-property}` | `--btn-color-text-decoration` |
36
+ | `--{component}-{property}-{state}` | `--btn-color-background-hover` |
37
+
38
+ ## Rules
39
+
40
+ **Lowercase kebab-case**, always.
41
+
42
+ **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.
43
+
44
+ **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`.
45
+
46
+ **`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`.
47
+
48
+ | Token | Role | CSS property it drives |
49
+ | --- | --- | --- |
50
+ | `--btn-color-background` | background | `background-color` |
51
+ | `--btn-color-border` | border | `border-color` |
52
+ | `--btn-color-text` | text | `color` |
53
+ | `--btn-color-text-decoration` | text-decoration | `text-decoration-color` |
54
+ | `--form-color-accent` | accent | `color` |
55
+ | `--input-color-placeholder` | placeholder | `color` |
56
+
57
+ **States go last**: `-hover`, `-focus`, `-active`, `-disabled`, `-checked`.
58
+
59
+ **Alphabetical order within a file**, grouped with a comment once a file has many entries.
60
+
61
+ ```css
62
+ /* Brand */
63
+ --color-brand: var(--color-sienna-600);
64
+ --color-brand-hover: var(--color-sienna-700);
65
+
66
+ /* Text */
67
+ --color-text: var(--color-gray-900);
68
+ --color-text-muted: var(--color-gray-500);
69
+ ```
70
+
71
+ ## The `default` convention
72
+
73
+ 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`.
74
+
75
+ ```json
76
+ "brand": {
77
+ "default": { "$value": "{color.sienna.600}", "$type": "color" },
78
+ "hover": { "$value": "{color.sienna.700}", "$type": "color" }
79
+ }
80
+ ```
81
+
82
+ 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.
83
+
84
+ ## Scales
85
+
86
+ | Use case | Scale | Example |
87
+ | --- | --- | --- |
88
+ | Color palettes | Numeric, 50 to 950 | `--color-gray-500` |
89
+ | Heading levels | Zero-padded, 01 to 06 | `--font-size-heading-01` |
90
+ | Layout and spacing | T-shirt, `2xs xs sm md lg xl 2xl` | `--spacing-md` |
91
+ | Radius, shadow, size | T-shirt | `--radius-sm` |
92
+ | Purposeful aliases | Named | `--radius-control`, `--radius-pill` |
93
+
94
+ 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.
95
+
96
+ ## Category reference
97
+
98
+ | Category | Covers | Example |
99
+ | --- | --- | --- |
100
+ | `color` | All color values | `--color-brand`, `--color-text-on-dark` |
101
+ | `font-family` | Typefaces | `--font-family-heading` |
102
+ | `font-size` | Text sizes | `--font-size-sm`, `--font-size-heading-01` |
103
+ | `font-size-fluid` | Responsive fluid type scale | `--font-size-fluid-xl` |
104
+ | `font-weight` | Weight values | `--font-weight-bold` |
105
+ | `line-height` | Line heights | `--line-height-heading` |
106
+ | `letter-spacing` | Tracking | `--letter-spacing-md` |
107
+ | `text-decoration` | Decoration properties | `--text-decoration-offset` |
108
+ | `spacing` | Margin and padding | `--spacing-md`, `--spacing-section` |
109
+ | `spacing-fluid` | Responsive fluid spacing scale | `--spacing-fluid-lg` |
110
+ | `size` | Width and height | `--size-16`, `--size-tablet` |
111
+ | `max-width` | Readability caps | `--max-width-paragraph` |
112
+ | `radius` | Border radius | `--radius-md`, `--radius-pill` |
113
+ | `border` | Border style and width | `--border-width-sm` |
114
+ | `shadow` | Box shadows | `--shadow-md` |
115
+ | `blur` | Blur values | `--blur-md` |
116
+ | `gradient` | Overlay gradients | `--gradient-darken-color-from` |
117
+ | `opacity` | Opacity values | `--opacity-disabled` |
118
+ | `duration` | Animation timing | `--duration-fast` |
119
+ | `easing` | Timing functions | `--easing-out-expo` |
120
+ | `transition` | Shorthand transitions | `--transition-normal` |
121
+ | `ratio` | Aspect ratios | `--ratio-video` |
122
+ | `focus` | Focus ring tokens | `--focus-outline-width` |
123
+ | `grid` | Column counts, gaps, fractions | `--columns-tablet`, `--gap` |
124
+ | `icon` | SVG icons as data URIs | `--icon-arrow` |
125
+ | `z-index` | Stacking order | `--z-index-modal` |
126
+
127
+ 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,128 @@
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
+ ---
7
+
8
+ 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.
9
+
10
+ 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.
11
+
12
+ {{< alert-block state="info" >}}
13
+ 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/`.
14
+ {{< /alert-block >}}
15
+
16
+ ## Primitive
17
+
18
+ Raw, context-free values. These answer "what is the value?" and carry no opinion about usage.
19
+
20
+ ### Color palette
21
+
22
+ 19 hues at 11 steps, plus black and white. See [Colors](../colors/) for the hue table and the step guide.
23
+
24
+ {{< tokens pkg="design" file="primitive/color" >}}
25
+
26
+ ### Size scale
27
+
28
+ {{< tokens pkg="design" file="primitive/size" >}}
29
+
30
+ ### Font primitives
31
+
32
+ {{< tokens pkg="design" file="primitive/font" >}}
33
+
34
+ ### Shadow scale
35
+
36
+ {{< tokens pkg="design" file="primitive/shadow" >}}
37
+
38
+ ### Blur scale
39
+
40
+ {{< tokens pkg="design" file="primitive/blur" >}}
41
+
42
+ ## Semantic
43
+
44
+ Named by purpose. These answer "what is this value for?" and are the layer a project should override.
45
+
46
+ ### Color roles
47
+
48
+ {{< tokens pkg="design" file="semantic/color" >}}
49
+
50
+ ### Typography
51
+
52
+ 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.
53
+
54
+ {{< tokens pkg="design" file="semantic/typography" >}}
55
+
56
+ ### Spacing
57
+
58
+ {{< tokens pkg="design" file="semantic/spacing" >}}
59
+
60
+ ### Size aliases
61
+
62
+ {{< tokens pkg="design" file="semantic/size" >}}
63
+
64
+ ### Grid
65
+
66
+ {{< tokens pkg="design" file="semantic/grid" >}}
67
+
68
+ ### Radius
69
+
70
+ {{< tokens pkg="design" file="semantic/radius" >}}
71
+
72
+ ### Border
73
+
74
+ {{< tokens pkg="design" file="semantic/border" >}}
75
+
76
+ ### Shadow aliases
77
+
78
+ {{< tokens pkg="design" file="semantic/shadow" >}}
79
+
80
+ ### Blur aliases
81
+
82
+ {{< tokens pkg="design" file="semantic/blur" >}}
83
+
84
+ ### Gradient
85
+
86
+ {{< tokens pkg="design" file="semantic/gradient" >}}
87
+
88
+ ### Opacity
89
+
90
+ {{< tokens pkg="design" file="semantic/opacity" >}}
91
+
92
+ ### Motion
93
+
94
+ Durations, easing curves and the transition shorthands built from them.
95
+
96
+ {{< tokens pkg="design" file="semantic/motion" >}}
97
+
98
+ ### Ratio
99
+
100
+ {{< tokens pkg="design" file="semantic/ratio" >}}
101
+
102
+ ### Focus
103
+
104
+ {{< tokens pkg="design" file="semantic/focus" >}}
105
+
106
+ ### Form
107
+
108
+ 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.
109
+
110
+ {{< tokens pkg="design" file="semantic/form" >}}
111
+
112
+ ### Icon
113
+
114
+ SVG icons encoded as `url('data:image/svg+xml;utf8,...')`, usable as a `background-image` or a `mask-image`.
115
+
116
+ {{< tokens pkg="design" file="semantic/icon" >}}
117
+
118
+ ### Z-index
119
+
120
+ {{< tokens pkg="design" file="semantic/z-index" >}}
121
+
122
+ ## Themes
123
+
124
+ ### Dark
125
+
126
+ The overlay applied under `prefers-color-scheme: dark`. See [Dark mode](../dark-mode/) for how the selector works and how to opt out.
127
+
128
+ {{< tokens pkg="design" file="themes/dark" >}}
@@ -0,0 +1,181 @@
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
+ ---
7
+
8
+
9
+ [Style Dictionary v5](https://styledictionary.com/) transforms the DTCG JSON token files into CSS custom properties.
10
+
11
+ ## Run the build
12
+
13
+ ```bash
14
+ npm run build
15
+ ```
16
+
17
+ Output is written to `dist/css/`. One CSS file is generated per JSON source file, preserving the same directory structure:
18
+
19
+ ```
20
+ tokens/primitive/color.json → dist/css/primitive/color.css
21
+ tokens/semantic/color.json → dist/css/semantic/color.css
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Config — `style-dictionary.config.js`
27
+
28
+ 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.
29
+
30
+ ### Name transform — `name/kebab/strip-default`
31
+
32
+ Converts the token path to a kebab-case CSS custom property name, dropping any `default` segment:
33
+
34
+ ```
35
+ color.background.default → --color-background
36
+ color.text.muted → --color-text-muted
37
+ ```
38
+
39
+ ### Format — `css/layer-tokens`
40
+
41
+ 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.
42
+
43
+ References are preserved as `var()` — tokens are **not** resolved to their final values:
44
+
45
+ ```css
46
+ /* output */
47
+ --color-text: var(--color-gray-900); /* not oklch(0.208 0.006 264.542) */
48
+ ```
49
+
50
+ 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:
51
+
52
+ ```json
53
+ { "$value": "{size.8} {size.12}" }
54
+ ```
55
+
56
+ ```css
57
+ --file-button-padding: var(--size-8) var(--size-12);
58
+ ```
59
+
60
+ ### Composite tokens
61
+
62
+ 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()`.
63
+
64
+ **Property order matters** — list properties in the order CSS expects them. For `box-shadow`:
65
+
66
+ ```json
67
+ {
68
+ "shadow": {
69
+ "sm": {
70
+ "$type": "shadow",
71
+ "$value": [
72
+ {
73
+ "offsetX": "0",
74
+ "offsetY": "1px",
75
+ "blur": "2px",
76
+ "spread": "0",
77
+ "color": "{color.shadow.light}",
78
+ "inset": false
79
+ }
80
+ ]
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ ```css
87
+ --shadow-sm: 0 1px 2px 0 var(--color-shadow-light);
88
+ ```
89
+
90
+ `inset` is always treated as a boolean prefix (`inset` or nothing), not a positional value.
91
+
92
+ ---
93
+
94
+ ## Dark mode — `themes/dark.json`
95
+
96
+ Dark mode is a **separate token set**, `tokens/themes/dark.json` — **not** declared inline on tokens (there is no `$mods`/`$modes` extension).
97
+
98
+ The model is **default + override**:
99
+
100
+ - 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.
101
+ - `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()`.
102
+
103
+ 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.
104
+
105
+ ### Generated output
106
+
107
+ The build emits `dist/css/themes/dark.css` with a single rule, inside `@layer tokens`:
108
+
109
+ ```css
110
+ @layer tokens {
111
+ @media (prefers-color-scheme: dark) {
112
+ :root:not([data-color-scheme="light"]) {
113
+ --color-background: var(--color-gray-950);
114
+ /* … */
115
+ }
116
+ }
117
+ }
118
+ ```
119
+
120
+ 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.
121
+
122
+ ### Activation matrix
123
+
124
+ 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).
125
+
126
+ | `data-color-scheme` on `<html>` | OS preference | Result | Why |
127
+ | --- | --- | --- | --- |
128
+ | *(none)* | light | **light** | `:root` defaults; the media query is inactive |
129
+ | *(none)* | dark | **dark** | the media rule matches `:root` (not forced light) |
130
+ | `light` | dark | **light** | `:not([data-color-scheme="light"])` excludes it → `:root` defaults |
131
+ | `light` | light | **light** | `:root` defaults; the media query is inactive |
132
+ | *(any other)* | dark | **dark** | the media rule still matches — only `light` opts out |
133
+
134
+ > **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.
135
+
136
+ ### Rules
137
+
138
+ - Only colour tokens that actually change between modes belong in `themes/dark.json`; everything mode-invariant stays in `semantic`.
139
+ - Dark values follow the same `{dotted.path}` reference syntax as `$value` (e.g. `{color.gray.950}`).
140
+
141
+ ---
142
+
143
+ ## Adding a new token file
144
+
145
+ 1. Create a JSON file anywhere under `tokens/` with DTCG structure.
146
+ 2. Run `npm run build` — the file is detected automatically.
147
+ 3. A matching CSS file is generated in `dist/css/`.
148
+
149
+ No changes to `style-dictionary.config.js` are needed.
150
+
151
+ ---
152
+
153
+ ## References
154
+
155
+ - [Style Dictionary v5 docs](https://styledictionary.com/)
156
+ - [DTCG format](DTCG.md) — token structure and types
157
+ - [Utopia fluid scales](UTOPIA.md) — `clamp()` values in spacing and typography tokens
158
+
159
+ ---
160
+
161
+ ## The JSON manifest
162
+
163
+ Alongside the CSS, the build writes `dist/tokens.json`: a flat array of every token the package ships.
164
+
165
+ ```json
166
+ [
167
+ {
168
+ "name": "--color-brand",
169
+ "value": "var(--color-sienna-600)",
170
+ "type": "color",
171
+ "file": "semantic/color",
172
+ "description": ""
173
+ }
174
+ ]
175
+ ```
176
+
177
+ The documentation site renders its reference tables from this file, which is why the reference cannot drift from the stylesheets.
178
+
179
+ 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.
180
+
181
+ `description` comes from the DTCG `$description` key. Adding one to a token source makes it appear in the published reference with no other change.