@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/README.md +52 -441
- package/dist/tokens.json +4629 -0
- package/docs/_index.md +119 -0
- package/docs/colors.md +218 -0
- package/docs/customizing.md +77 -0
- package/docs/dark-mode.md +96 -0
- package/docs/dtcg.md +231 -0
- package/docs/naming.md +127 -0
- package/docs/reference.md +128 -0
- package/docs/style-dictionary.md +181 -0
- package/docs/utopia.md +54 -0
- package/package.json +3 -1
package/docs/_index.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: design-tokens
|
|
4
|
+
description: Framework-agnostic primitive and semantic design tokens, authored in DTCG JSON and shipped as CSS custom properties.
|
|
5
|
+
weight: 2
|
|
6
|
+
icon: palette
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Design tokens are the atomic decisions of a design system: colors, spacing, typography, motion. Instead of hardcoding `#ae003f` or `1rem` throughout a codebase, you name the decision, `--color-brand` or `--spacing-md`, and reference that name everywhere.
|
|
10
|
+
|
|
11
|
+
`@uncinq/design-tokens` holds the primitive and semantic layers of that vocabulary. It is authored in [DTCG JSON](dtcg/) and compiled to CSS custom properties by [Style Dictionary](style-dictionary/). It works anywhere CSS does: Hugo, Symfony, Shopify, or a plain HTML page.
|
|
12
|
+
|
|
13
|
+
## Token architecture
|
|
14
|
+
|
|
15
|
+
The package follows the DTCG three-layer model.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
primitive → semantic → component
|
|
19
|
+
(raw values) (purpose) (component-scoped, not in this package)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Layer 1, primitive
|
|
23
|
+
|
|
24
|
+
Raw, context-free values. No opinion about where they are used.
|
|
25
|
+
|
|
26
|
+
```css
|
|
27
|
+
--color-indigo-600: oklch(0.511 0.262 276.966);
|
|
28
|
+
--size-16: 1rem;
|
|
29
|
+
--font-weight-bold: 700;
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
A primitive token answers **"what is the value?"**.
|
|
33
|
+
|
|
34
|
+
### Layer 2, semantic
|
|
35
|
+
|
|
36
|
+
Named by purpose rather than by appearance. References a primitive through `var()`.
|
|
37
|
+
|
|
38
|
+
```css
|
|
39
|
+
--color-brand: var(--color-sienna-600);
|
|
40
|
+
--spacing-md: var(--size-30);
|
|
41
|
+
--font-weight-heading: var(--font-weight-bold);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
A semantic token answers **"what is this value for?"**.
|
|
45
|
+
|
|
46
|
+
This is the layer that gives portability. Every project consuming this package shares the same semantic API, so when the brand color changes you update one primitive and every semantic token referencing it follows.
|
|
47
|
+
|
|
48
|
+
### Layer 3, component
|
|
49
|
+
|
|
50
|
+
Scoped to a single component. The generic ones live in [@uncinq/component-tokens](https://github.com/uncinq/component-tokens); project-specific ones belong in each project's own design system.
|
|
51
|
+
|
|
52
|
+
```css
|
|
53
|
+
--alert-border-radius: var(--radius-none);
|
|
54
|
+
--btn-padding-inline: var(--spacing-control);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm install @uncinq/design-tokens
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```css
|
|
64
|
+
/* everything */
|
|
65
|
+
@import '@uncinq/design-tokens';
|
|
66
|
+
|
|
67
|
+
/* or by layer */
|
|
68
|
+
@import '@uncinq/design-tokens/css/primitive.css';
|
|
69
|
+
@import '@uncinq/design-tokens/css/semantic.css';
|
|
70
|
+
|
|
71
|
+
/* or file by file */
|
|
72
|
+
@import '@uncinq/design-tokens/css/semantic/color.css';
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Without a build step, over a CDN:
|
|
76
|
+
|
|
77
|
+
```html
|
|
78
|
+
<link rel="stylesheet" href="https://unpkg.com/@uncinq/design-tokens">
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Every generated file declares `@layer tokens` itself, so the package never fixes the layer order. Declaring that order is the consuming project's job, and it has to happen before any import. See [cascade layers in css-base](../css-base/cascade-layers/).
|
|
82
|
+
|
|
83
|
+
## Where to go next
|
|
84
|
+
|
|
85
|
+
| Page | Covers |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| [Naming](naming/) | The naming grammar, the rules, the scales, the full category list |
|
|
88
|
+
| [Colors](colors/) | OKLCH, the primitive palette, semantic color roles, WCAG guidance |
|
|
89
|
+
| [Dark mode](dark-mode/) | How the dark theme overlay works and how to opt out |
|
|
90
|
+
| [Customizing](customizing/) | The two override strategies and when each applies |
|
|
91
|
+
| [Reference](reference/) | Every token, generated from the JSON sources |
|
|
92
|
+
| [DTCG format](dtcg/) | The authoring format |
|
|
93
|
+
| [Style Dictionary](style-dictionary/) | The build pipeline |
|
|
94
|
+
| [Fluid scales](utopia/) | The Utopia method behind the fluid tokens |
|
|
95
|
+
|
|
96
|
+
## File structure
|
|
97
|
+
|
|
98
|
+
The JSON sources under `tokens/` are the only files to edit. Everything under `dist/` is generated by `npm run build`.
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
tokens/
|
|
102
|
+
primitive/ blur, color, font, shadow, size
|
|
103
|
+
semantic/ blur, border, color, focus, form, gradient, grid, icon,
|
|
104
|
+
motion, opacity, radius, ratio, shadow, size, spacing,
|
|
105
|
+
typography, z-index
|
|
106
|
+
themes/ dark
|
|
107
|
+
|
|
108
|
+
dist/css/
|
|
109
|
+
index.css imports primitive, semantic, themes/dark
|
|
110
|
+
primitive.css barrel
|
|
111
|
+
semantic.css barrel
|
|
112
|
+
primitive/ semantic/ themes/ one CSS file per JSON source
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## References
|
|
116
|
+
|
|
117
|
+
- [DTCG specification](https://tr.designtokens.org/format/), W3C Community Group draft
|
|
118
|
+
- [Style Dictionary v5](https://styledictionary.com/)
|
|
119
|
+
- [Utopia](https://utopia.fyi), the fluid scale method
|
package/docs/colors.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Colors
|
|
4
|
+
description: The OKLCH color space, the 19-hue primitive palette, the semantic color roles, and WCAG guidance for using them.
|
|
5
|
+
weight: 2
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Color space, OKLCH
|
|
9
|
+
|
|
10
|
+
Every primitive color is defined in OKLCH.
|
|
11
|
+
|
|
12
|
+
| Channel | Range | Meaning |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| `L` | 0 to 1 | Perceptual lightness, 0 is black and 1 is white |
|
|
15
|
+
| `C` | 0 to about 0.4 | Chroma, or colorfulness, where 0 is gray |
|
|
16
|
+
| `H` | 0 to 360 degrees | Hue angle |
|
|
17
|
+
|
|
18
|
+
Why OKLCH rather than hex or HSL:
|
|
19
|
+
|
|
20
|
+
- **Perceptually uniform.** Equal steps in `L` produce equal perceived brightness differences, whatever the hue. HSL does not guarantee this: `hsl(60, 100%, 50%)` yellow looks far brighter than `hsl(240, 100%, 50%)` blue at the same stated lightness.
|
|
21
|
+
- **Predictable contrast.** You can reason about WCAG contrast by comparing `L` values, without converting to relative luminance first.
|
|
22
|
+
- **Better interpolation.** Gradients and animations between two OKLCH colors do not pass through muddy grays.
|
|
23
|
+
- **Composable.** The relative color syntax, `oklch(from var(--color-text) l c h / 0.6)`, lets a token derive from another without duplicating its value. Several semantic tokens in this package rely on it.
|
|
24
|
+
|
|
25
|
+
Browser support is Chrome 111, Firefox 113 and Safari 15.4 and above. No sRGB fallback is generated, so every value ships as `oklch()` and anything older needs a fallback of its own.
|
|
26
|
+
|
|
27
|
+
## Primitive palette
|
|
28
|
+
|
|
29
|
+
19 hues, 11 steps each (50 to 950), plus flat `--color-black` and `--color-white`. These are raw values with no opinion about usage.
|
|
30
|
+
|
|
31
|
+
| Hue | H angle | Character |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| `amber` | 70 | Golden yellow-orange |
|
|
34
|
+
| `blue` | 260 | Classic blue |
|
|
35
|
+
| `cyan` | 215 | Bright cyan |
|
|
36
|
+
| `emerald` | 162 | Deep blue-green |
|
|
37
|
+
| `fuchsia` | 322 | Vivid magenta |
|
|
38
|
+
| `gray` | 264 | Cool neutral, chroma about 0.02 |
|
|
39
|
+
| `green` | 150 | Lush green |
|
|
40
|
+
| `indigo` | 277 | Blue-violet |
|
|
41
|
+
| `lime` | 131 | Electric yellow-green |
|
|
42
|
+
| `orange` | 48 | Vivid orange |
|
|
43
|
+
| `pink` | 354 | Bright pink |
|
|
44
|
+
| `purple` | 304 | Rich purple |
|
|
45
|
+
| `red` | 25 | Classic red |
|
|
46
|
+
| `rose` | 16 | Pink-red |
|
|
47
|
+
| `sienna` | 24 | Brick-red, the default brand |
|
|
48
|
+
| `sky` | 237 | Soft sky blue |
|
|
49
|
+
| `teal` | 183 | Blue-green |
|
|
50
|
+
| `violet` | 293 | Modern violet |
|
|
51
|
+
| `yellow` | 86 | Pure yellow |
|
|
52
|
+
|
|
53
|
+
### Step guide
|
|
54
|
+
|
|
55
|
+
`L` values below are measured from the actual palette, not estimated.
|
|
56
|
+
|
|
57
|
+
| Step | L, cool hues | L, amber / yellow / lime | L, gray | Typical use |
|
|
58
|
+
| --- | --- | --- | --- | --- |
|
|
59
|
+
| 50 | 0.97 | 0.99 | 0.98 | Tinted page backgrounds, hover on white |
|
|
60
|
+
| 100 | 0.95 | 0.97 | 0.97 | Muted backgrounds, badges, tags |
|
|
61
|
+
| 200 | 0.90 | 0.94 | 0.93 | Borders, dividers |
|
|
62
|
+
| 300 | 0.83 | 0.89 | 0.87 | Disabled elements, placeholder text |
|
|
63
|
+
| 400 | 0.73 | 0.84 | 0.71 | Secondary icons, decorative |
|
|
64
|
+
| 500 | 0.66 | 0.78 | 0.56 | Mid-tone, pair with dark text |
|
|
65
|
+
| 600 | 0.58 | 0.67 | 0.44 | **Default brand and status background.** White text passes WCAG AA for UI |
|
|
66
|
+
| 700 | 0.50 | 0.55 | 0.37 | Hover state, colored text on white |
|
|
67
|
+
| 800 | 0.44 | 0.47 | 0.28 | Deep accents, high-contrast text |
|
|
68
|
+
| 900 | 0.39 | 0.41 | 0.21 | Near-dark, very high contrast |
|
|
69
|
+
| 950 | 0.27 | 0.28 | 0.14 | Darkest tint, almost black |
|
|
70
|
+
|
|
71
|
+
Two things to read out of that table. Intrinsically bright hues (amber, yellow, lime) carry noticeably higher `L` at steps 400 to 700; that is expected behaviour of a perceptual space, not a calibration error. Gray goes the other way and runs darker than the chromatic hues from step 400 down, because it has almost no chroma to contribute to perceived brightness.
|
|
72
|
+
|
|
73
|
+
## Semantic color tokens
|
|
74
|
+
|
|
75
|
+
Semantic tokens are named by purpose and reference primitives through `var()`.
|
|
76
|
+
|
|
77
|
+
### Brand and accent
|
|
78
|
+
|
|
79
|
+
```css
|
|
80
|
+
--color-brand /* primary brand color: button background, active states */
|
|
81
|
+
--color-brand-hover /* hover state */
|
|
82
|
+
--color-brand-muted /* tinted background for brand areas */
|
|
83
|
+
--color-brand-strong /* darkest brand shade */
|
|
84
|
+
|
|
85
|
+
--color-accent /* equals brand by default, override independently if needed */
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The default brand is **sienna**, a warm brick-red. `--color-accent` and its variants alias brand, so overriding brand alone moves both.
|
|
89
|
+
|
|
90
|
+
```css
|
|
91
|
+
@layer tokens {
|
|
92
|
+
:root {
|
|
93
|
+
--color-brand: var(--color-violet-600);
|
|
94
|
+
--color-brand-hover: var(--color-violet-700);
|
|
95
|
+
--color-brand-muted: var(--color-violet-100);
|
|
96
|
+
--color-brand-strong: var(--color-violet-900);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Backgrounds
|
|
102
|
+
|
|
103
|
+
| Token | Default | Usage |
|
|
104
|
+
| --- | --- | --- |
|
|
105
|
+
| `--color-background` | white | Page background |
|
|
106
|
+
| `--color-background-surface` | equals `--color-background` | Card and panel backgrounds |
|
|
107
|
+
| `--color-background-muted` | gray-100 | Subtle section backgrounds |
|
|
108
|
+
| `--color-background-muted-hover` | gray-200 | Hover on a muted surface |
|
|
109
|
+
| `--color-background-media` | gray-200 | Image placeholders, skeletons |
|
|
110
|
+
| `--color-background-disabled` | gray-100 | Disabled controls |
|
|
111
|
+
| `--color-background-accent` | equals `--color-accent` | Highlighted sections |
|
|
112
|
+
|
|
113
|
+
### Text
|
|
114
|
+
|
|
115
|
+
| Token | Default | Usage |
|
|
116
|
+
| --- | --- | --- |
|
|
117
|
+
| `--color-text` | gray-900 | Body text |
|
|
118
|
+
| `--color-text-hover` | gray-700 | Text hover |
|
|
119
|
+
| `--color-text-muted` | `--color-text` at `--opacity-muted` | Secondary text, captions |
|
|
120
|
+
| `--color-text-disabled` | gray-300 | Disabled UI |
|
|
121
|
+
| `--color-heading` | black | Headings |
|
|
122
|
+
| `--color-link` | equals `--color-text` | Default link color |
|
|
123
|
+
| `--color-link-hover` | equals `--color-accent` | Link hover |
|
|
124
|
+
| `--color-link-active` | equals `--color-active` | Current link |
|
|
125
|
+
| `--color-credit` | equals `--color-text-muted` | Bylines, captions |
|
|
126
|
+
|
|
127
|
+
`--color-text-muted` is derived rather than aliased: `oklch(from var(--color-text) l c h / var(--opacity-muted))`. It therefore follows any override of `--color-text` automatically, which is why it is not simply an alias of gray-500. The dark theme is the one place that overrides it outright, to gray-400, because a transparent text color over a dark background does not mute the same way it does over a light one.
|
|
128
|
+
|
|
129
|
+
### Text on colored backgrounds
|
|
130
|
+
|
|
131
|
+
These exist so that contrast holds when a color becomes the background.
|
|
132
|
+
|
|
133
|
+
```css
|
|
134
|
+
--color-text-on-brand /* white */
|
|
135
|
+
--color-text-on-accent /* white */
|
|
136
|
+
--color-text-on-dark /* white */
|
|
137
|
+
--color-text-on-black /* white */
|
|
138
|
+
--color-text-on-light /* gray-900 */
|
|
139
|
+
--color-text-on-white /* black */
|
|
140
|
+
--color-text-on-surface /* equals --color-text */
|
|
141
|
+
--color-text-on-muted /* gray-900 */
|
|
142
|
+
--color-text-on-neutral /* white */
|
|
143
|
+
--color-text-on-danger /* white */
|
|
144
|
+
--color-text-on-info /* white */
|
|
145
|
+
--color-text-on-success /* white */
|
|
146
|
+
--color-text-on-highlight /* gray-900 */
|
|
147
|
+
--color-text-on-warning /* gray-900, amber is bright so dark text is required */
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Status and surface variants
|
|
151
|
+
|
|
152
|
+
| Token | Primitive | Notes |
|
|
153
|
+
| --- | --- | --- |
|
|
154
|
+
| `--color-danger` | red-600 | Errors, destructive actions |
|
|
155
|
+
| `--color-success` | green-600 | Confirmations |
|
|
156
|
+
| `--color-warning` | amber-500 | Warnings, pair with `--color-text-on-warning` |
|
|
157
|
+
| `--color-info` | blue-600 | Informational |
|
|
158
|
+
| `--color-highlight` | yellow-100 | Marked or highlighted text |
|
|
159
|
+
| `--color-neutral` | gray-500 | Neutral emphasis |
|
|
160
|
+
| `--color-dark` | gray-900 | Dark surfaces |
|
|
161
|
+
| `--color-light` | gray-200 | Light surfaces |
|
|
162
|
+
|
|
163
|
+
Each of these carries a `-hover`, `-muted` and `-strong` companion:
|
|
164
|
+
|
|
165
|
+
```css
|
|
166
|
+
--color-danger: var(--color-red-600);
|
|
167
|
+
--color-danger-hover: var(--color-red-700);
|
|
168
|
+
--color-danger-muted: var(--color-red-100);
|
|
169
|
+
--color-danger-strong: var(--color-red-800);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Derived utility colors
|
|
173
|
+
|
|
174
|
+
Four tokens use relative color syntax rather than an alias, so they track their base automatically:
|
|
175
|
+
|
|
176
|
+
| Token | Derived from |
|
|
177
|
+
| --- | --- |
|
|
178
|
+
| `--color-backdrop` | `--color-black` at `--opacity-backdrop` |
|
|
179
|
+
| `--color-shadow-light` | `--color-shadow` at `--opacity-shadow` |
|
|
180
|
+
| `--color-shadow-medium` | `--color-shadow` at `--opacity-backdrop` |
|
|
181
|
+
| `--color-shadow-strong` | `--color-shadow` at `--opacity-overlay` |
|
|
182
|
+
|
|
183
|
+
`--color-shadow` is one of the tokens the dark theme flips, from black to white. Because the three shadow variants derive from it, the whole shadow system inverts with a single override. See [Dark mode](../dark-mode/).
|
|
184
|
+
|
|
185
|
+
## Accessibility
|
|
186
|
+
|
|
187
|
+
| Ratio | Requirement |
|
|
188
|
+
| --- | --- |
|
|
189
|
+
| 4.5 : 1 | Normal text, under 18px, or non-bold under 14px. WCAG AA |
|
|
190
|
+
| 3 : 1 | Large text and UI components such as buttons, inputs, icons. WCAG AA |
|
|
191
|
+
| 7 : 1 | Any text. WCAG AAA |
|
|
192
|
+
|
|
193
|
+
Rules of thumb for this palette:
|
|
194
|
+
|
|
195
|
+
- **White text on a colored background** needs step **600 or darker**. Step 500 and below typically land around 3 to 3.5 : 1, which fails for normal text.
|
|
196
|
+
- **Colored text on white** needs step **700 or darker** for normal text.
|
|
197
|
+
- **Warning** is the exception that proves the rule. `--color-warning` is amber-500, whose `L` is about 0.78, so it must be paired with `--color-text-on-warning` (gray-900). Never put white text on it.
|
|
198
|
+
- **Decorative use** is unconstrained. Any step is fine when color carries no information, as in borders, illustrations or icons that duplicate a visible label.
|
|
199
|
+
|
|
200
|
+
These are starting points, not a substitute for measuring. Contrast depends on both colors, and an override of `--color-brand` invalidates the assumptions above.
|
|
201
|
+
|
|
202
|
+
## Adding a custom hue
|
|
203
|
+
|
|
204
|
+
Add a primitive scale in `tokens/primitive/color.json`, one DTCG color object per step:
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
{
|
|
208
|
+
"color": {
|
|
209
|
+
"coral": {
|
|
210
|
+
"50": { "$value": { "colorSpace": "oklch", "components": [0.975, 0.014, 35.0] }, "$type": "color" },
|
|
211
|
+
"100": { "$value": { "colorSpace": "oklch", "components": [0.948, 0.032, 35.0] }, "$type": "color" },
|
|
212
|
+
"950": { "$value": { "colorSpace": "oklch", "components": [0.225, 0.078, 35.0] }, "$type": "color" }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Run `npm run build`, then reference the generated `--color-coral-*` from `tokens/semantic/color.json`, or from your own project's `@layer tokens` override. Keeping `H` constant across the steps and varying `L` and `C` is what makes a scale read as one hue.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Customizing
|
|
4
|
+
description: The two override strategies, CSS layer override and JSON plus build, and when each one is the right tool.
|
|
5
|
+
weight: 4
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
There are two ways to change what this package produces. Pick by how deep the change goes, not by preference.
|
|
9
|
+
|
|
10
|
+
| Need | Approach |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| Change the brand, the typography, a handful of tokens | CSS `@layer tokens` |
|
|
13
|
+
| Add a few project-specific tokens | CSS `@layer tokens` |
|
|
14
|
+
| Redefine the primitive palette entirely | JSON plus build |
|
|
15
|
+
| Add many project tokens, or generate other formats | JSON plus build |
|
|
16
|
+
|
|
17
|
+
## 1. CSS override
|
|
18
|
+
|
|
19
|
+
Every token lives in `@layer tokens`, the lowest-priority layer in the recommended order. Any `@layer tokens` block imported **after** this package wins on source order, with no specificity tricks required.
|
|
20
|
+
|
|
21
|
+
```css
|
|
22
|
+
@import '@uncinq/design-tokens';
|
|
23
|
+
|
|
24
|
+
@layer tokens {
|
|
25
|
+
:root {
|
|
26
|
+
--color-brand: var(--color-violet-600);
|
|
27
|
+
--color-brand-hover: var(--color-violet-700);
|
|
28
|
+
--color-brand-muted: var(--color-violet-100);
|
|
29
|
+
--color-brand-strong: var(--color-violet-900);
|
|
30
|
+
--font-family-sans: 'Inter', system-ui, sans-serif;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This covers most cases. Note that you are overriding the *semantic* layer while still referencing the *primitive* layer, which is exactly what the two-layer split is for: the palette stays intact and you only restate the intent.
|
|
36
|
+
|
|
37
|
+
Three things worth knowing:
|
|
38
|
+
|
|
39
|
+
**Override the semantic token, not the component token,** when you want the change to propagate. Setting `--color-brand` moves every component that reads it. Setting `--btn-color-background` moves only buttons.
|
|
40
|
+
|
|
41
|
+
**Derived tokens follow automatically.** `--color-accent` aliases `--color-brand`, and `--color-text-muted` derives from `--color-text`, so overriding the base moves the derivations too. Check [Colors](../colors/) before overriding a derived token by hand, because doing so breaks the link.
|
|
42
|
+
|
|
43
|
+
**The layer order must already be declared.** If `@layer tokens` has not been named before the first import, the cascade fixes its position at that first import and your override may not land where you expect. See [cascade layers](../../css-base/cascade-layers/).
|
|
44
|
+
|
|
45
|
+
## 2. JSON plus build
|
|
46
|
+
|
|
47
|
+
The package exports its raw DTCG sources under `./tokens/*`. A consuming project can feed them into its own [Style Dictionary](https://styledictionary.com/) config alongside its own token files.
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
// style-dictionary.config.js, in the consuming project
|
|
51
|
+
export default {
|
|
52
|
+
usesDtcg: true,
|
|
53
|
+
source: [
|
|
54
|
+
'node_modules/@uncinq/design-tokens/tokens/**/*.json',
|
|
55
|
+
'tokens/**/*.json', // project tokens, extending or overriding the package
|
|
56
|
+
],
|
|
57
|
+
// ...
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
A project token file that defines the same path as a package token overrides it during the build. New paths are additive.
|
|
62
|
+
|
|
63
|
+
Use this approach to:
|
|
64
|
+
|
|
65
|
+
- Redefine the primitive palette entirely, for a different brand hue or a different scale
|
|
66
|
+
- Add semantic tokens that do not exist in the package
|
|
67
|
+
- Generate output formats the package does not ship, such as JS, SCSS or JSON
|
|
68
|
+
|
|
69
|
+
That last point is the common reason to reach for it. This package emits **CSS only**, on purpose: CSS custom properties are the one format that works unchanged in Hugo, Symfony, Shopify and a plain HTML page. A project that needs the same values inside JavaScript should generate that itself from the same sources, so the two outputs cannot drift.
|
|
70
|
+
|
|
71
|
+
## What not to do
|
|
72
|
+
|
|
73
|
+
**Do not edit `dist/`.** It is generated by `npm run build` and overwritten on every build. The header of each generated file says so.
|
|
74
|
+
|
|
75
|
+
**Do not fork to change one color.** The CSS override above exists precisely so that a brand change does not require a fork. A fork inherits the maintenance of 19 hues and 660 tokens for a single line of difference.
|
|
76
|
+
|
|
77
|
+
**Do not override a primitive to change a semantic meaning.** Redefining `--color-gray-100` to make one muted background darker will move every other token referencing gray-100, most of which you have not thought about. Override the semantic token instead.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Dark mode
|
|
4
|
+
description: How the dark theme overlays 13 semantic tokens, how a page opts out, and why there is no forced-dark selector.
|
|
5
|
+
weight: 3
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
The dark theme is an **overlay**, not a second palette. `tokens/themes/dark.json` re-declares 13 semantic tokens and nothing else. Every other token, including the whole primitive palette, is shared.
|
|
9
|
+
|
|
10
|
+
That is the point of the semantic layer: if a component reads `--color-background` rather than `--color-white`, it needs no dark-mode branch of its own.
|
|
11
|
+
|
|
12
|
+
## What it overrides
|
|
13
|
+
|
|
14
|
+
| Token | Light | Dark |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| `--color-background` | white | gray-950 |
|
|
17
|
+
| `--color-background-disabled` | gray-100 | gray-900 |
|
|
18
|
+
| `--color-background-media` | gray-200 | gray-800 |
|
|
19
|
+
| `--color-background-muted` | gray-100 | gray-900 |
|
|
20
|
+
| `--color-background-muted-hover` | gray-200 | gray-800 |
|
|
21
|
+
| `--color-border` | gray-200 | gray-800 |
|
|
22
|
+
| `--color-heading` | black | white |
|
|
23
|
+
| `--color-shadow` | black | white |
|
|
24
|
+
| `--color-text` | gray-900 | gray-200 |
|
|
25
|
+
| `--color-text-disabled` | gray-300 | gray-600 |
|
|
26
|
+
| `--color-text-hover` | gray-700 | gray-300 |
|
|
27
|
+
| `--color-text-muted` | derived from `--color-text` | gray-400 |
|
|
28
|
+
| `--form-color-background` | white | gray-900 |
|
|
29
|
+
|
|
30
|
+
Two of these do more work than they look. `--color-shadow` flipping to white inverts the entire shadow system in one line, because `--color-shadow-light`, `-medium` and `-strong` all derive from it with relative color syntax. And `--color-background-surface` is not in the list at all, because it aliases `--color-background` and follows for free.
|
|
31
|
+
|
|
32
|
+
## The generated selector
|
|
33
|
+
|
|
34
|
+
```css
|
|
35
|
+
@layer tokens {
|
|
36
|
+
@media (prefers-color-scheme: dark) {
|
|
37
|
+
:root:not([data-color-scheme="light"]) {
|
|
38
|
+
--color-background: var(--color-gray-950);
|
|
39
|
+
/* ... */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Read that selector carefully, because it encodes two deliberate decisions.
|
|
46
|
+
|
|
47
|
+
**The dark scheme follows the operating system.** It activates under `prefers-color-scheme: dark` and nowhere else.
|
|
48
|
+
|
|
49
|
+
**There is no forced-dark selector.** No `[data-color-scheme="dark"]` exists, so the package will never turn a page dark on a light OS. Only the opposite is offered: `data-color-scheme="light"` on `<html>` opts a page out of dark mode even when the OS asks for it.
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<html data-color-scheme="light">
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
If your project needs a three-way toggle with a forced-dark option, add the missing selector in your own `@layer tokens` block after the import. The package deliberately stops short of that, because a forced-dark selector has to be paired with UI and persistence that belong to the project, not to a token package.
|
|
56
|
+
|
|
57
|
+
## Importing it
|
|
58
|
+
|
|
59
|
+
The theme ships with the full bundle:
|
|
60
|
+
|
|
61
|
+
```css
|
|
62
|
+
@import '@uncinq/design-tokens'; /* primitive + semantic + dark */
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Importing a group on its own leaves it out:
|
|
66
|
+
|
|
67
|
+
```css
|
|
68
|
+
@import '@uncinq/design-tokens/css/primitive.css';
|
|
69
|
+
@import '@uncinq/design-tokens/css/semantic.css';
|
|
70
|
+
/* no dark theme here */
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Add it back explicitly when you need it:
|
|
74
|
+
|
|
75
|
+
```css
|
|
76
|
+
@import '@uncinq/design-tokens/css/themes/dark.css';
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## How the build resolves it
|
|
80
|
+
|
|
81
|
+
The dark pass is a second Style Dictionary build, not a post-processing step. It sources the base tokens **and** `themes/dark.json` together, so references such as `{color.gray.950}` resolve normally, then filters the output down to the dark file alone.
|
|
82
|
+
|
|
83
|
+
Without that, a dark token referencing a primitive would emit a broken reference. See [Style Dictionary](../style-dictionary/) for the config.
|
|
84
|
+
|
|
85
|
+
## Adding another theme
|
|
86
|
+
|
|
87
|
+
Drop a JSON file in `tokens/themes/`, overriding only the tokens that change, then register its selector in `style-dictionary.config.js`:
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
'themes/dark': [{
|
|
91
|
+
selector: ':root:not([data-color-scheme="light"])',
|
|
92
|
+
media: '(prefers-color-scheme: dark)',
|
|
93
|
+
}]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
A theme with no `media` entry generates a plain selector, which is what a forced, user-chosen theme needs. Keep the overlay small: a theme that re-declares more than a couple of dozen tokens is usually a sign that a component is reading primitives directly instead of semantic tokens.
|