@fusion.dev/helix-docs 0.104.0 → 0.105.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.
Files changed (48) hide show
  1. package/content/accessibility.mdx +92 -71
  2. package/content/angular.mdx +26 -24
  3. package/content/browser-support.mdx +61 -58
  4. package/content/building-on-the-headless-tier.mdx +100 -74
  5. package/content/charting.mdx +98 -79
  6. package/content/coding-agents.mdx +87 -69
  7. package/content/color-modes.mdx +40 -37
  8. package/content/color.mdx +67 -54
  9. package/content/composition.mdx +85 -83
  10. package/content/elevation-and-material.mdx +32 -29
  11. package/content/events.mdx +77 -65
  12. package/content/forms.mdx +69 -58
  13. package/content/iconography.mdx +75 -71
  14. package/content/installation.mdx +77 -68
  15. package/content/intent-reference.mdx +59 -54
  16. package/content/introduction.mdx +25 -23
  17. package/content/localization.mdx +122 -82
  18. package/content/maps.mdx +222 -0
  19. package/content/metrics.mdx +294 -297
  20. package/content/motion.mdx +44 -36
  21. package/content/other-frameworks.mdx +33 -31
  22. package/content/overlays.mdx +72 -54
  23. package/content/overriding-a-component.mdx +85 -74
  24. package/content/react.mdx +72 -64
  25. package/content/recipes.mdx +64 -47
  26. package/content/routing.mdx +100 -93
  27. package/content/sandbox.mdx +39 -39
  28. package/content/server-rendering.mdx +95 -84
  29. package/content/shape-and-borders.mdx +44 -37
  30. package/content/size-and-density.mdx +51 -46
  31. package/content/space-and-size.mdx +41 -34
  32. package/content/state-and-control.mdx +63 -51
  33. package/content/surfaces-and-separation.mdx +55 -53
  34. package/content/svelte.mdx +20 -19
  35. package/content/theme-builder.mdx +42 -31
  36. package/content/theming.mdx +58 -51
  37. package/content/tiers.mdx +69 -61
  38. package/content/tokens.mdx +72 -53
  39. package/content/typography.mdx +47 -47
  40. package/content/using-helix-with-an-agent.mdx +147 -115
  41. package/content/using-the-elements.mdx +55 -50
  42. package/content/versioning.mdx +42 -35
  43. package/content/visual-axes.mdx +168 -146
  44. package/content/vue.mdx +24 -24
  45. package/content/without-a-bundler.mdx +40 -23
  46. package/dist/pages.generated.js +49 -43
  47. package/dist/sources.generated.js +3337 -2680
  48. package/package.json +3 -3
@@ -1,28 +1,26 @@
1
1
  ---
2
- description: Deriving an accessible token set from intent inputs.
2
+ description: Compute a complete token set from your brand color and a few other inputs.
3
3
  section: Theming
4
4
  title: Deriving a theme
5
5
  ---
6
6
 
7
7
  # Theming
8
8
 
9
- **Tokens are a function, not a table.**
9
+ Helix computes your color tokens from a few inputs, such as your brand color.
10
10
 
11
- A design system that ships two hundred hand-picked hex values can only offer
12
- override. You change one, break contrast somewhere three components away, and
13
- never find out. That is a config file, not a theming system.
11
+ When a design system ships two hundred fixed color values, you theme it by
12
+ changing them one at a time. A change to one value can break the contrast of a
13
+ component elsewhere, and nothing tells you.
14
14
 
15
- `@fusion.dev/helix-theme` computes a complete token set from a handful of intent
16
- inputs, and every foreground and background pair meets its contrast target **by
17
- construction** rather than by audit.
15
+ `@fusion.dev/helix-theme` computes a complete token set from a few intent
16
+ inputs, and every text and background pair meets its contrast target.
18
17
 
19
18
  ## Two ways to theme
20
19
 
21
- They are both real and they are not the same thing, so they have different
22
- names.
20
+ Helix has two ways to theme, and they give different guarantees.
23
21
 
24
- **Tuned** is the CSS path. Write a seed, the stylesheet re-derives, and nothing
25
- has to be installed or run:
22
+ **Tuned** uses CSS only. Set a seed and the stylesheet recomputes the colors,
23
+ with nothing to install or run:
26
24
 
27
25
  ```css
28
26
  :root {
@@ -32,19 +30,19 @@ has to be installed or run:
32
30
  }
33
31
  ```
34
32
 
35
- It works at `:root` and on any `[data-helix-theme]` subtree, and nowhere else.
36
- A `var()` inside a custom property is substituted on the element the property is
37
- *declared* on, so the ramp has already resolved at the root and a seed written
38
- on a plain element is read by nothing. Naming the scope is what re-runs those
39
- declarations.
33
+ Set seeds at `:root` or on an element with `[data-helix-theme]`. A seed on any
34
+ other element has no effect. A `var()` inside a custom property is substituted
35
+ on the element where the property is *declared*, so the ramp has already
36
+ resolved at the root. The stylesheet declares the ramp again on each
37
+ `[data-helix-theme]` element, so the ramp there reads the seeds you set on it.
40
38
 
41
- **Tuned makes no contrast promise**, and cannot. CSS has no contrast function:
42
- `oklch(from …)` can step and rotate, and it cannot search for the lightness that
43
- clears a target. The figures baked into the ramp are exact for the seed that
44
- ships and approximate for any other.
39
+ Tuned does not guarantee contrast. CSS has no contrast function.
40
+ `oklch(from …)` can step and rotate a color, but it cannot search for the
41
+ lightness that meets a target. The contrast figures built into the ramp are
42
+ exact for the default seed and approximate for any other.
45
43
 
46
- **Verified** is the engine. It solves rather than steps, measures every pair it
47
- emits, and hands back an artifact:
44
+ **Verified** uses the theme engine. It searches for each color that meets its
45
+ target, measures every pair, and returns the theme with any violations:
48
46
 
49
47
  ```ts
50
48
  import { deriveTheme, toCss } from '@fusion.dev/helix-theme'
@@ -62,42 +60,50 @@ if (theme.violations.length > 0) {
62
60
  console.log(toCss(theme))
63
61
  ```
64
62
 
65
- The emitted block carries the seeds as well as the tokens, so the halves the
66
- engine does not compute, the twenty-four series colors a chart reads, the
67
- spacing scale and the corner radii, re-derive from the same values the solver
68
- used. That is what stops the two paths disagreeing on the same page.
63
+ The CSS from `toCss` includes the seeds as well as the tokens. The stylesheet
64
+ computes the values the engine does not, from the same seeds the engine used:
69
65
 
70
- Reach for tuned to try a color. Reach for verified to ship one.
66
+ - The twenty-four series colors for charts
67
+ - The spacing scale
68
+ - The corner radii
71
69
 
72
- ## Why OKLCH
70
+ This keeps both ways of theming in agreement on the same page.
73
71
 
74
- Equal numeric steps in sRGB are not equal perceptual steps, which makes both
75
- scale generation and contrast math unreliable. OKLCH is what makes derivation
76
- possible at all. Colors are authored in it and converted at the edge.
72
+ Use tuned to try a color. Use verified for the theme you ship.
77
73
 
78
- The brand color keeps its hue and chroma; only lightness moves to meet the
79
- target. That is what preserves brand identity while guaranteeing contrast.
74
+ ## Why Helix uses OKLCH
80
75
 
81
- ## What it protects against
76
+ Equal numeric steps in sRGB do not look like equal steps. That makes scales and
77
+ contrast math in sRGB unreliable. Helix computes themes in OKLCH for this
78
+ reason. Colors are written in OKLCH and converted to other formats on output.
82
79
 
83
- Both of these produce a palette that passes review and fails in production.
84
- Both are covered by regression tests.
80
+ The engine keeps your brand color's hue and chroma and changes only its
81
+ lightness to meet the target. Your brand color stays recognizable, and its
82
+ contrast is guaranteed.
85
83
 
86
- **Solving against colors that cannot be rendered.** A near-white surface
87
- carrying brand tint falls outside the sRGB gamut and gets truncated on the way
88
- to hex, breaking both the intended hue and the contrast just solved for. Every
89
- color is gamut-fitted before it is solved against, and a token reports
90
- `chromaReduced` when fitting changed it rather than clipping silently.
84
+ ## Errors the engine prevents
91
85
 
92
- **Verifying against only one surface.** Text lands on `surface` as often as on
93
- `background`: cards, dialogs, menus. In a dark theme `surface` is *lighter*
94
- than `background`, so light text loses contrast inside every card while the
95
- page itself passes. Foregrounds are solved against the hardest surface they
96
- land on, not just the page background.
86
+ The engine prevents two errors. Each one produces a palette that passes review
87
+ and fails in production. Regression tests cover both.
97
88
 
98
- ## Inspecting a theme
89
+ ### Colors outside the sRGB gamut
99
90
 
100
- Every derived token carries the check that produced it:
91
+ A near-white surface with a brand tint can fall outside the sRGB gamut.
92
+ Converting it to hex then clips it, which changes its hue and breaks the
93
+ contrast the engine solved for. The engine fits every color into the gamut
94
+ before it solves against that color. When fitting changes a token, the token
95
+ reports `chromaReduced`.
96
+
97
+ ### Text on more than one surface
98
+
99
+ Text appears on `surface` as often as on `background`, for example in cards,
100
+ dialogs, and menus. In a dark theme, `surface` is *lighter* than `background`.
101
+ Light text that passes on the page can lose contrast inside every card. The
102
+ engine solves each foreground color against the hardest surface it appears on.
103
+
104
+ ## Inspect a theme
105
+
106
+ Each derived token includes the result of its contrast check:
101
107
 
102
108
  ```ts
103
109
  const theme = deriveTheme({ brand })
@@ -107,5 +113,6 @@ for (const token of theme.tokens) {
107
113
  }
108
114
  ```
109
115
 
110
- `violations` is empty when the theme passes. Anything that could not reach its
111
- target at the requested chroma is reported there rather than shipped.
116
+ `violations` is empty when the theme passes. A color that cannot reach its
117
+ target at the requested chroma is reported in `violations`, so you can fix it
118
+ before you ship.
package/content/tiers.mdx CHANGED
@@ -7,39 +7,47 @@ title: Tiers
7
7
 
8
8
  # Tiers
9
9
 
10
- Helix ships as four stacked packages. Each has one job, and the dependency
11
- direction never reverses.
10
+ Helix is four packages stacked on each other. Each package has one job, and each
11
+ depends only on the packages below it.
12
12
 
13
13
  | Package | Owns | Depends on |
14
14
  | --- | --- | --- |
15
15
  | `@fusion.dev/helix-styles` | Tokens and the base layer. Pure CSS | nothing |
16
16
  | `@fusion.dev/helix-core` | Element base class, mixins, decorators | Lit |
17
17
  | `@fusion.dev/helix-headless` | Structure, accessibility, state, form participation | `core` |
18
- | `@fusion.dev/helix` | Presentation: tokens, parts, and the visual axes | `headless`, `turbo run styles` |
18
+ | `@fusion.dev/helix` | Presentation: tokens, parts, and the visual axes | `headless`, `styles` |
19
19
 
20
- `@fusion.dev/helix-theme` and `@fusion.dev/helix-icons` ship beside the stack rather than in it:
21
- neither is a tier, and neither is needed to render a component. **There is no
22
- React package.** The framework typings are generated into `@fusion.dev/helix`, and
23
- the bridge that repairs React's hydration of custom events is in
24
- `@fusion.dev/helix-core`. See [React](./react).
20
+ `@fusion.dev/helix-theme` and `@fusion.dev/helix-icons` ship beside the stack.
21
+ Neither is a tier, and you do not need either one to render a component.
25
22
 
26
- ## Why the split
23
+ There is no React package. We generate the framework typings into
24
+ `@fusion.dev/helix`, and the bridge that repairs React's hydration of custom
25
+ events is in `@fusion.dev/helix-core`. See [React](./react).
27
26
 
28
- **`headless` must never depend on `turbo run styles`.**
27
+ ## Why the tiers are separate packages
29
28
 
30
- That single constraint is what makes the headless tier independently
31
- consumable. Consume it and bring your own
32
- styling entirely; nothing about how Helix looks comes with it. If styling leaks
33
- downward, the four are one package in four files.
29
+ **`headless` must never depend on `styles`.**
34
30
 
35
- ## What belongs where
31
+ This rule lets you use the headless tier on its own. You install it and write
32
+ all of the styling yourself. Nothing about how Helix looks comes with it. If
33
+ styling moved down into the headless tier, the four packages would be one
34
+ package split across four files.
36
35
 
37
- The headless tier owns what a component **is and does**: markup structure, ARIA
38
- wiring, slots, events, internal state, form participation. It ships no CSS, no
39
- token references, and none of the visual axes.
36
+ ## What each tier contains
40
37
 
41
- The styled tier extends it and adds only presentation. Token references live
42
- here.
38
+ The headless tier owns what a component is and does:
39
+
40
+ - markup structure
41
+ - ARIA wiring
42
+ - slots
43
+ - events
44
+ - internal state
45
+ - form participation
46
+
47
+ It ships no CSS, no token references, and none of the visual axes.
48
+
49
+ The styled tier extends the headless tier and adds only presentation. Token
50
+ references live in the styled tier.
43
51
 
44
52
  ```ts
45
53
  // headless/src/accordion/item/accordion-item.ts
@@ -77,10 +85,11 @@ export class HelixAccordion extends HelixHeadlessAccordion {
77
85
  }
78
86
  ```
79
87
 
80
- ## Styling from the outside
88
+ ## Style a component from your stylesheet
81
89
 
82
- Shadow DOM markup uses CSS Shadow Parts, not class names. Every structural
83
- element carries a `part`, which doubles as the public override API:
90
+ Elements inside the shadow DOM carry no class names for you to select. Each
91
+ structural element carries a `part`, and parts are the public API for overriding
92
+ styles:
84
93
 
85
94
  ```css
86
95
  helix-accordion-item::part(trigger) {
@@ -88,9 +97,9 @@ helix-accordion-item::part(trigger) {
88
97
  }
89
98
  ```
90
99
 
91
- For single-knob changes, prefer the per-component custom property. It defaults
92
- to a system token, so overriding it restyles one component without touching
93
- `--helix-*` and affecting everything else:
100
+ To change one value, set the component's own custom property. It defaults to a
101
+ system token. Setting it restyles that one component and leaves `--helix-*` and
102
+ every other component unchanged:
94
103
 
95
104
  ```css
96
105
  helix-accordion {
@@ -98,16 +107,16 @@ helix-accordion {
98
107
  }
99
108
  ```
100
109
 
101
- ## The window before an element upgrades
110
+ ## Hide elements until they upgrade
102
111
 
103
- A custom element is an inline, unstyled box until its definition loads, and its
104
- light-DOM children render immediately in document order. So a card arrives as a
105
- run of unstyled text and then snaps into a card. The window is short and it is
106
- most visible on a server-rendered page, where the markup is there long before
107
- the JavaScript is.
112
+ A custom element is an inline, unstyled box until its definition loads. Its
113
+ light-DOM children render immediately, in document order. A card first appears
114
+ as unstyled text and then changes into a card. The delay is short. It is most
115
+ visible on a server-rendered page, where the markup arrives long before the
116
+ JavaScript.
108
117
 
109
- `@fusion.dev/helix` closes it with `elements.css`, which hides every one of its
110
- tags until the definition lands:
118
+ `@fusion.dev/helix` includes `elements.css`, which hides each of its tags until
119
+ the tag's definition loads:
111
120
 
112
121
  ```css
113
122
  helix-card:not(:defined) {
@@ -115,22 +124,22 @@ helix-card:not(:defined) {
115
124
  }
116
125
  ```
117
126
 
118
- **The base tier ships no such file, and that is deliberate rather than
119
- missing.** Three reasons, and the third is the one that matters to you.
127
+ **The base tier ships no such file on purpose.** There are three reasons, and
128
+ the third one affects you most.
120
129
 
121
- It is a decision rather than a mechanism. The rules the library ships below the
122
- styled tier are the ones a component cannot work without: `overflow` is what
123
- creates a scrollport, `[hidden]` is the platform's own semantics. Hiding an
124
- element that has not upgraded is a preference between two imperfect frames, and
125
- the content is in the markup either way, so a crawler reads it regardless.
130
+ First, hiding an element is a design choice. Below the styled tier, we ship only
131
+ the rules a component cannot work without. For example, `overflow` creates a
132
+ scrollport, and `[hidden]` is the platform's own semantics. Hiding an element
133
+ that has not upgraded is a choice between two imperfect first frames. The
134
+ content is in the markup either way, so a crawler reads it in both cases.
126
135
 
127
- It is a document rule, not a component one. Nothing can help from inside,
128
- because before the upgrade there is no shadow root to put a rule in. It has to
129
- live in your stylesheet, which makes it yours to decide.
136
+ Second, the rule belongs to the document. A component cannot apply it from
137
+ inside, because there is no shadow root before the upgrade. The rule has to
138
+ live in your stylesheet, so the choice is yours.
130
139
 
131
- And a blanket file would be wrong for you anyway. You import the handful of
132
- base elements you use, not all of them, so a generated list would hide tags you
133
- never ship. Write the ones you have:
140
+ Third, one file for every tag would not fit your project. You import only the
141
+ base elements you use, so a generated list would include tags you never ship.
142
+ Write the rule for the elements you have:
134
143
 
135
144
  ```css
136
145
  helix-base-accordion:not(:defined),
@@ -139,21 +148,20 @@ helix-base-dialog:not(:defined) {
139
148
  }
140
149
  ```
141
150
 
142
- `visibility` rather than `display`, so the element keeps its place in the
143
- layout and the page does not reflow when it upgrades. `display: none` collapses
144
- the box and the content jumps in when the definition arrives, which trades a
145
- flash of unstyled text for a shift of everything under it.
151
+ Use `visibility` so the element keeps its place in the layout and the page does
152
+ not reflow when it upgrades. `display: none` collapses the box, and the content
153
+ jumps in when the definition arrives. That replaces a flash of unstyled text
154
+ with a shift of everything below it.
146
155
 
147
- You may want neither. A page whose first paint matters more than its last
148
- inch, or one that has to be readable with JavaScript disabled, is better off
149
- letting the unstyled markup show. That choice is the reason this file is not
150
- made for you.
156
+ You may want to hide nothing. If your first paint matters more than the final
157
+ layout, or your page has to be readable with JavaScript turned off, let the
158
+ unstyled markup show. Because of that choice, we do not write the file for you.
151
159
 
152
160
  ## Mixins and tooling
153
161
 
154
- `open` and `defaultOpen` come from `MixinControllerOpen` in `@fusion.dev/helix-core`.
155
- The manifest analyzer cannot follow a mixin into another package, so any
156
- mixin-provided API a component exposes has to be documented on the component
157
- itself with `@event`, `@property`, or similar. Undocumented mixin API is
158
- invisible to IDE tooltips, the React wrapper generator, and any agent reading
159
- the manifest.
162
+ `open` and `defaultOpen` come from `MixinControllerOpen` in
163
+ `@fusion.dev/helix-core`. The manifest analyzer cannot follow a mixin into
164
+ another package. Document any API that a component gets from a mixin on the
165
+ component itself, with `@event`, `@property`, or a similar tag. Mixin API
166
+ without that documentation is missing from IDE tooltips, from the React wrapper
167
+ generator, and from the manifest that agents read.
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: The two layers, the surface ladder, and what is enforced.
2
+ description: The two token layers, the surface tokens, and the rules the build enforces.
3
3
  pinned: true
4
4
  section: Theming
5
5
  title: Tokens
@@ -7,45 +7,59 @@ title: Tokens
7
7
 
8
8
  # Tokens
9
9
 
10
- Two layers. You see the second.
10
+ Helix has two layers of tokens. You use the second layer, the semantic tokens.
11
11
 
12
12
  | Layer | Shape | Who may use it |
13
13
  |---|---|---|
14
14
  | Primitive | `--_helix-neutral-500` | `@fusion.dev/helix-styles` only |
15
- | Semantic | `--helix-color-primary` | everything, including you |
16
-
17
- Every semantic token answers *what is this for*, never *what value is it*. There
18
- are fifteen categories and every token is in exactly one: `color` `space`
19
- `radii` `font` `text` `weight` `leading` `tracking` `border` `shadow`
20
- `material` `duration` `easing` `size` `layer`.
21
-
22
- The leading underscore is the whole convention: `--_helix-` is private and may
23
- be renamed or re-spaced without notice. Reaching past the semantic layer is the
24
- one thing that will break under you.
25
-
26
- ## The surfaces
27
-
28
- Depth is distance from the page, in whichever direction there is room. The page
29
- is white in light and black in dark, so in light there is nothing above white to
30
- be lifted into: a raised surface goes *down* the ramp, not up.
15
+ | Semantic | `--helix-color-primary` | Any code, including yours |
16
+
17
+ Each semantic token is named for its purpose. The name does not describe its
18
+ value. Every token belongs to exactly one of fifteen categories:
19
+
20
+ - `color`
21
+ - `space`
22
+ - `radii`
23
+ - `font`
24
+ - `text`
25
+ - `weight`
26
+ - `leading`
27
+ - `tracking`
28
+ - `border`
29
+ - `shadow`
30
+ - `material`
31
+ - `duration`
32
+ - `easing`
33
+ - `size`
34
+ - `layer`
35
+
36
+ The leading underscore marks a token as private. We may rename or re-space any
37
+ `--_helix-` token without notice. Code that uses a primitive token can break
38
+ when you update Helix, so use the semantic tokens.
39
+
40
+ ## Surface tokens
41
+
42
+ A surface's depth is its distance from the page, in whichever direction has
43
+ room. The page is white in light mode and black in dark mode. In light mode
44
+ nothing is lighter than white, so a raised surface is *darker* than the page.
31
45
 
32
46
  - **`background`**: the page.
33
- - **`surface`**: a plane on the page. A card, a panel you own.
34
- - **`surface-raised`**: something lifted while still *lying on* the page: a
35
- selected tab's pill, a hovered row, an option under the pointer. It has the
36
- page on all sides, so it is told apart by its fill, which on white has to be a
37
- step darker.
38
- - **`surface-overlay`**: something *over* the page rather than on it: a menu, a
39
- popover, a dialog, and the thumb of a switch or a segmented control. It is
40
- told apart by its shadow and by what it covers, so on white it is white.
41
- - **`surface-sunken`**: a well you put a value into, an input's fill, a track, a
42
- groove.
43
-
44
- **`raised` and `overlay` are the pair worth learning.** They land on the same
45
- step in dark mode, so one token looks like it would do both jobs. In light they
46
- want opposite fills: a hovered row must be darker than a white page, and a thumb
47
- must be lighter than the groove it sits in. If something looks almost right in
48
- dark and wrong in light, this is usually why.
47
+ - **`surface`**: a plane on the page, such as a card or a panel you own.
48
+ - **`surface-raised`**: something lifted that still *lies on* the page, such as
49
+ a selected tab's pill, a hovered row, or an option under the pointer. The page
50
+ surrounds it on all sides, so its fill sets it apart. On a white page, that
51
+ fill is one step darker.
52
+ - **`surface-overlay`**: something *over* the page, such as a menu, a popover, a
53
+ dialog, or the thumb of a switch or a segmented control. Its shadow and the
54
+ content it covers set it apart, so on a white page it is white.
55
+ - **`surface-sunken`**: a well that holds a value, such as an input's fill, a
56
+ track, or a groove.
57
+
58
+ **Use `raised` and `overlay` for different jobs.** They are the same color in
59
+ dark mode, so one token can seem to do both jobs. In light mode they need
60
+ opposite fills. A hovered row must be darker than a white page, and a thumb must
61
+ be lighter than the groove it sits in. If a surface looks almost right in dark
62
+ mode and wrong in light mode, it usually uses the wrong one of these two tokens.
49
63
 
50
64
  ## Per-component hooks
51
65
 
@@ -55,34 +69,39 @@ helix-card {
55
69
  }
56
70
  ```
57
71
 
58
- `--card-padding` is not a Helix token. It is a hook that particular component
59
- exposes, documented on the component, and it defaults to a semantic token. Use
60
- them to nudge one component; use the semantic tokens to move everything.
72
+ `--card-padding` is a hook that the card component exposes. Hooks are separate
73
+ from the Helix tokens. Each component documents its own hooks, and each hook
74
+ defaults to a semantic token. Use a hook to adjust one component. Use the
75
+ semantic tokens to change every component.
61
76
 
62
- ## What is enforced
77
+ ## What fails the build
63
78
 
64
- Four things fail the build:
79
+ The build fails on four kinds of error:
65
80
 
66
- 1. **A raw value where a token exists.** `#e5e5e5` when a neutral is right there.
67
- 2. **A primitive referenced from outside `@fusion.dev/helix-styles`.**
68
- 3. **A reference to a token that does not exist.**
69
- 4. **A component hook nothing reads**, and a primitive nothing resolves into.
81
+ 1. A raw value where a token exists, such as `#e5e5e5` where a neutral token
82
+ fits.
83
+ 2. A primitive token used outside `@fusion.dev/helix-styles`.
84
+ 3. A reference to a token that does not exist.
85
+ 4. A component hook that nothing reads, or a primitive token that nothing
86
+ resolves to.
70
87
 
71
- The third earns its keep on its own: a renamed token resolves to nothing, the
72
- property silently falls back to a browser default, and the code looks correct
73
- while the component is quietly unstyled.
88
+ The third check would be worth running on its own. When a token is renamed, a
89
+ reference to the old name resolves to nothing. The property falls back to a
90
+ browser default without a warning. The code looks correct, and the component
91
+ loses its styles.
74
92
 
75
- ## Theming
93
+ ## Color modes
76
94
 
77
- Both modes live in one declaration:
95
+ Each token declares its light and dark values in one declaration:
78
96
 
79
97
  ```css
80
98
  --helix-color-surface: light-dark(var(--_helix-neutral-50), var(--_helix-neutral-900));
81
99
  ```
82
100
 
83
- Which half resolves is decided by `color-scheme` and nothing else, so the color
84
- mode is a single attribute and no token is restated to honor it. Set
85
- `data-color-mode="light"` or `"dark"` on any element to theme that subtree.
86
- `color-scheme` inherits, so a light preview inside a dark page is one attribute.
101
+ The `color-scheme` property alone decides which value applies. You set the
102
+ color mode with one attribute, and no token is declared twice. Set
103
+ `data-color-mode="light"` or `"dark"` on any element to set the mode for that
104
+ element and everything inside it. `color-scheme` inherits, so one attribute
105
+ shows a light preview inside a dark page.
87
106
 
88
- Absent, it follows the operating system.
107
+ Without the attribute, the mode follows the operating system.