@fusion.dev/helix-docs 0.104.1 → 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,14 +1,15 @@
1
1
  ---
2
- description: Why a declared property is a seed, and how to take a decision when you want it.
2
+ description: How a property you set becomes a starting value, and how to cancel or take over a change.
3
3
  section: Essentials
4
4
  title: State and control
5
5
  ---
6
6
 
7
7
  # State and control
8
8
 
9
- **A declared property is a seed and a correction.** Write `value`, `open`,
10
- `checked` or `active` on a Helix element and it starts there and goes on
11
- managing itself, exactly as `<input value="Ada">` does.
9
+ A property you set is a starting value, and you can assign it again later to
10
+ correct it. Set `value`, `open`, `checked`, or `active` on a Helix element. The
11
+ element starts there and then manages its own state, as `<input value="Ada">`
12
+ does.
12
13
 
13
14
  ```html
14
15
  <helix-input
@@ -17,20 +18,20 @@ managing itself, exactly as `<input value="Ada">` does.
17
18
  ></helix-input>
18
19
  ```
19
20
 
20
- That works. Type in it and the value moves. Assign to it later and the value
21
- moves to what you assigned. There is no mode to be in and nothing to wire before
22
- the control responds.
21
+ When you type in the input, the value changes. When you assign a new value
22
+ later, the value changes to what you assigned. The control has no modes, and you
23
+ do not need to connect anything before it responds.
23
24
 
24
- *This page said the opposite until 26 Aug 2026: writing a property was a claim
25
- of ownership, so the element announced its changes and applied none of them
26
- until it was told. The mode is deleted. If you were refusing a change by
27
- withholding a state update rather than by calling `preventDefault()`, that is
28
- the one thing that has to be rewritten.*
25
+ *Until August 26, 2026, this page said the opposite. Setting a property claimed
26
+ ownership of the state, so the element announced its changes and applied none
27
+ of them until you set the new value. That mode is removed. If you canceled
28
+ changes by withholding a state update, rewrite that code to call
29
+ `preventDefault()`. Nothing else has to change.*
29
30
 
30
- ## Taking a decision
31
+ ## Cancel or take over a change
31
32
 
32
- Control is an action rather than a mode. Every change announces before it acts,
33
- and the announcement is cancelable:
33
+ You take control one change at a time. Each change sends a cancelable event
34
+ before it is applied:
34
35
 
35
36
  ```ts
36
37
  input.addEventListener('helix-before-change', (event) => {
@@ -40,8 +41,8 @@ input.addEventListener('helix-before-change', (event) => {
40
41
  })
41
42
  ```
42
43
 
43
- `preventDefault()` refuses that one change and nothing moves. To own the state
44
- outright, refuse and assign your own:
44
+ `preventDefault()` cancels that one change, and nothing moves. To manage the
45
+ state yourself, cancel the change and assign your own value:
45
46
 
46
47
  ```tsx
47
48
  <helix-input
@@ -54,22 +55,27 @@ outright, refuse and assign your own:
54
55
  />
55
56
  ```
56
57
 
57
- This is the same shape the platform uses everywhere else: `preventDefault()` on
58
- a form submit, on a link click, on `beforeinput`. Nothing is declared in advance
59
- and nothing is inferred from which properties you happened to set.
58
+ The platform uses the same pattern. You call `preventDefault()` to cancel:
60
59
 
61
- The imperative form reports the same thing:
60
+ - a form submit
61
+ - a link click
62
+ - a `beforeinput` event
63
+
64
+ You do not declare anything in advance, and Helix does not infer anything from
65
+ which properties you set.
66
+
67
+ The imperative method reports the same result:
62
68
 
63
69
  ```ts
64
70
  const applied = item.requestOpen(true)
65
71
  ```
66
72
 
67
- `false` means something refused it.
73
+ `false` means something canceled the change.
68
74
 
69
- ## Mirroring the state instead
75
+ ## Follow the state with the change event
70
76
 
71
- Most consumers want neither of the above. Bind the notification and let the
72
- element apply:
77
+ Most of the time you do not need to cancel anything. Listen for the change
78
+ event and let the element apply the change:
73
79
 
74
80
  ```tsx
75
81
  <helix-input label="Name" onChange={(event) => setName(event.currentTarget.value)} />
@@ -82,11 +88,11 @@ element apply:
82
88
  ></helix-input>
83
89
  ```
84
90
 
85
- The native event carries no detail because it does not need one: the element has
86
- already applied the change by the time it fires, so the value is on the target
87
- where every handler ever written expects to find it.
91
+ The native event carries no detail. The element has already applied the change
92
+ when the event fires, so the value is on the target, where handlers expect to
93
+ find it.
88
94
 
89
- ## `default-` still means something
95
+ ## `default-value` sets the value a reset returns to
90
96
 
91
97
  ```html
92
98
  <helix-input
@@ -95,38 +101,44 @@ where every handler ever written expects to find it.
95
101
  ></helix-input>
96
102
  ```
97
103
 
98
- `default-value` is what a reset returns to. `value` is where the control is now.
99
- The pair is the platform's, where a native input reflects its `value` *content
100
- attribute* to `defaultValue` and carries a dirty flag that stops the attribute
101
- affecting the value once a reader has typed.
104
+ `default-value` is the value a reset returns to. `value` is the control's
105
+ current value. The pair comes from the platform. A native input reflects its
106
+ `value` *content attribute* to `defaultValue`, and it keeps a dirty flag. Once a
107
+ user has typed, the flag stops the attribute from changing the value.
102
108
 
103
- ## State is a custom state, not an attribute
109
+ ## State is published as a custom state
104
110
 
105
111
  ```css
106
112
  helix-accordion-item:state(open) { … }
107
113
  ```
108
114
 
109
- `open` is not reflected, and neither is `value`. A payload written back as an
110
- attribute leaks whatever the last reader typed into the markup, so a serialized
111
- host comes back holding their text rather than the author's seed, with no way to
112
- tell the two apart. `helix-input` takes `type="password"`, which a reflected
113
- `value` would carry straight past every session-replay masker that knows to mask
114
- `.value` and `input[value]`.
115
+ `open` is not reflected to an attribute, and neither is `value`. A value written
116
+ back as an attribute would put whatever the last user typed into the markup. A
117
+ serialized element would then hold the user's text, and you could not tell it
118
+ apart from the starting value you wrote. `helix-input` takes `type="password"`.
119
+ A reflected `value` would carry the password past every session-replay masker
120
+ that masks `.value` and `input[value]`.
121
+
122
+ Helix publishes the resolved boolean as a custom state, and the attribute keeps
123
+ the value you wrote. There are four custom states:
124
+
125
+ - `:state(open)`
126
+ - `:state(checked)`
127
+ - `:state(invalid)`
128
+ - `:state(read)`
115
129
 
116
- So the resolved boolean is published as a custom state and the plain attribute
117
- stays what you wrote. `:state(open)`, `:state(checked)`, `:state(invalid)` and
118
- `:state(read)` are the four, and they work from your own stylesheet exactly as
119
- an attribute selector did:
130
+ They work in your own stylesheet as an attribute selector did:
120
131
 
121
132
  ```css
122
133
  helix-checkbox:state(checked)::part(control) { … }
123
134
  helix-input:state(invalid)::part(field) { … }
124
135
  ```
125
136
 
126
- **These were `data-open` and `data-checked` until 27 Aug 2026.** An attribute is
127
- in the DOM, and the element writes it on its first update, so a framework that
128
- server-renders your markup and then hydrates it finds attributes its own render
129
- never produced. React reports that as a hydration mismatch and does not patch it
130
- up. A custom state is not in the DOM: it reaches your stylesheet and leaves the
131
- markup alone, so the two renders agree by construction. If you were selecting on
132
- the old names, the change is mechanical.
137
+ Until August 27, 2026, these states were the `data-open` and `data-checked`
138
+ attributes. An attribute is in the DOM, and the element writes it on its first
139
+ update. A framework that renders your markup on the server and then hydrates it
140
+ finds attributes that its own render never produced. React reports that as a
141
+ hydration mismatch and does not fix it. A custom state is not in the DOM. It
142
+ reaches your stylesheet and leaves the markup alone, so the server render and
143
+ the client render always match. If you selected on the old names, replace each
144
+ one with its custom state.
@@ -1,99 +1,101 @@
1
1
  ---
2
- description: The four ways a surface is told apart, and the ground a component is not.
2
+ description: The four ways to separate a surface from what is behind it, and the tokens that choose between them.
3
3
  section: Foundations
4
4
  title: Surfaces and separation
5
5
  ---
6
6
 
7
7
  # Surfaces and separation
8
8
 
9
- **A surface is told apart from what is behind it by exactly one of four things:
10
- a fill, an edge, a lift, or space.** A design language leads with one of them.
11
- Helix leads with the edge on the page and with the lift above it: a card takes a
12
- hairline and no shadow, a menu takes a shadow and no hairline. The ladder below
13
- decides what a surface sits on.
9
+ **Separate each surface from what is behind it with exactly one of four
10
+ things.** The four are a fill, an edge, a lift, and space. A design language
11
+ leads with one of them. Helix uses an edge for surfaces on the page and a lift
12
+ for surfaces above it. A card has a hairline border and no shadow. A menu has a
13
+ shadow and no hairline border. The surface ladder below sets the ground color
14
+ for each place a surface can sit.
14
15
 
15
- ## The four mechanisms
16
+ ## The four ways to separate a surface
16
17
 
17
18
  | | |
18
19
  |---|---|
19
- | **Fill** | A different ground color to what is behind it |
20
+ | **Fill** | A ground color different from what is behind it |
20
21
  | **Edge** | A border |
21
22
  | **Lift** | A shadow |
22
- | **Space** | Nothing at all, and the gap does the work |
23
+ | **Space** | Nothing at all. The gap separates it |
23
24
 
24
- Most systems use two or three without deciding, which is how you end up with a
25
- card that has a border, a shadow *and* a tint, each added by someone solving the
26
- same problem again.
25
+ When a design system uses two or three of these without a plan, a card can end
26
+ up with a border, a shadow, and a tint, each added by someone solving the same
27
+ problem again.
27
28
 
28
- Helix expresses the choice in two pairs of policy tokens:
29
+ Helix sets the choice with two pairs of policy tokens:
29
30
 
30
31
  [//]: # (tokens:separation-policy)
31
32
 
32
33
  | Token | Value | For |
33
34
  | --- | --- | --- |
34
- | `--helix-border-surface` | `var(--helix-border-thin)` | The edge a surface in the flow takes. Zero to go shadow-first. |
35
- | `--helix-shadow-surface` | `var(--helix-shadow-none)` | The lift a surface in the flow takes. None to go border-first. |
36
- | `--helix-border-floating` | `0` | The edge a surface over content takes. |
37
- | `--helix-shadow-floating` | `var(--helix-shadow-lg)` | The lift a surface over content takes. |
35
+ | `--helix-border-surface` | `var(--helix-border-thin)` | The border of a surface in the page flow. Set it to zero to separate surfaces with shadows. |
36
+ | `--helix-shadow-surface` | `var(--helix-shadow-none)` | The shadow of a surface in the page flow. Set it to none to separate surfaces with borders. |
37
+ | `--helix-border-floating` | `0` | The border of a surface that floats over content. |
38
+ | `--helix-shadow-floating` | `var(--helix-shadow-lg)` | The shadow of a surface that floats over content. |
38
39
 
39
40
  [//]: # (/tokens)
40
41
 
41
- Set the border pair to zero and the shadow pair to something, and the whole
42
- library goes shadow-first. Reverse them and it goes border-first. Every surface
43
- in the library is themeable in both directions, and checked, because a
44
- half-converted one reads as done: hook the shadow and hardcode `border: none`
45
- and it themes perfectly in one direction and cannot be moved in the other.
42
+ To make every component shadow-first, set the border pair to zero and give the
43
+ shadow pair a value. Reverse them to go border-first. Every Helix surface reads
44
+ both pairs, and this is checked. A surface that reads only one pair is easy to
45
+ miss. If it reads the shadow token and sets `border: none` directly, a theme can
46
+ change it in one direction and not in the other.
46
47
 
47
- ## The ladder
48
+ ## The surface ladder
48
49
 
49
- A surface's ground says where it is, not what shade it is:
50
+ A surface's ground color token names where the surface sits:
50
51
 
51
52
  [//]: # (tokens:surface-ladder)
52
53
 
53
54
  | Token | Sample | For |
54
55
  | --- | --- | --- |
55
- | `--helix-color-background` | <span data-token-swatch style={{ background: 'var(--helix-color-background)' }} /> | The page. Told apart by nothing, because it is the ground. |
56
- | `--helix-color-surface` | <span data-token-swatch style={{ background: 'var(--helix-color-surface)' }} /> | A plane on the page: a card, or a panel you own. Told apart by a fill, an edge or a lift, whichever the policy leads with. |
57
- | `--helix-color-surface-raised` | <span data-token-swatch style={{ background: 'var(--helix-color-surface-raised)' }} /> | Lifted while still lying on the page: a selected tab, a hovered row. Told apart by its fill, which goes down the ramp rather than up. |
58
- | `--helix-color-surface-overlay` | <span data-token-swatch style={{ background: 'var(--helix-color-surface-overlay)' }} /> | Over the page rather than on it: a menu, a popover, a dialog. Told apart by its shadow and by what it covers. |
59
- | `--helix-color-surface-sunken` | <span data-token-swatch style={{ background: 'var(--helix-color-surface-sunken)' }} /> | A well a value goes into: an input, a track, a groove. Told apart by its fill, inward. |
56
+ | `--helix-color-background` | <span data-token-swatch style={{ background: 'var(--helix-color-background)' }} /> | The page background. Every other surface sits on it. |
57
+ | `--helix-color-surface` | <span data-token-swatch style={{ background: 'var(--helix-color-surface)' }} /> | A surface on the page, such as a card or a panel. The separation policy sets it apart with a fill, a border, or a shadow. |
58
+ | `--helix-color-surface-raised` | <span data-token-swatch style={{ background: 'var(--helix-color-surface-raised)' }} /> | A raised surface on the page, such as a selected tab or a hovered row. Its fill is one step down the ramp. |
59
+ | `--helix-color-surface-overlay` | <span data-token-swatch style={{ background: 'var(--helix-color-surface-overlay)' }} /> | A surface over the page, such as a menu, a popover, or a dialog. Its shadow sets it apart. |
60
+ | `--helix-color-surface-sunken` | <span data-token-swatch style={{ background: 'var(--helix-color-surface-sunken)' }} /> | A recessed surface that holds a value, such as an input, a track, or a groove. Its fill sets it apart. |
60
61
 
61
62
  [//]: # (/tokens)
62
63
 
63
- **`raised` goes down the ramp, not up**, which is the counterintuitive one. A
64
- selected tab's pill, a hovered row and an option under the pointer all have the
65
- page on every side, so they are told apart by their fill, and on a white page a
66
- fill has to be a step *darker* to be seen. `overlay` is the opposite: it is told
67
- apart by its shadow and by covering something, so on white it is white.
64
+ **`raised` goes one step down the ramp.** A selected tab's pill, a hovered row,
65
+ and an option under the pointer all have the page on every side. Only their
66
+ fill separates them, and on a white page a fill has to be a step darker to be
67
+ seen. `overlay` works the other way. Its shadow and what it covers separate it,
68
+ so on a white page it is white.
68
69
 
69
- ## The common mistake
70
+ ## Using the page color inside a component
70
71
 
71
- **`--helix-color-background` is the page, and your component is not the page.**
72
+ **`--helix-color-background` is the page color. Do not use it inside a
73
+ component.**
72
74
 
73
75
  ```css
74
76
  /* Wrong. Claims the component is sitting on the page. */
75
77
  .marker { background: var(--helix-color-background); }
76
78
  ```
77
79
 
78
- Every use of the page's color inside a component is a claim about what that
79
- component is sitting on, and the claim is right exactly as often as the
80
- component is put on the page and no other ground. `Timeline`'s marker made it,
81
- painting itself the page color to knock the rail out behind it, and came out
82
- darker than any surface that was not the page: on a card it showed as a black
83
- disc. The sparkline's ring terminus made the same mistake and looked identical.
80
+ Using the page color inside a component assumes the component sits on the page.
81
+ That is right only when the component is placed directly on the page.
84
82
 
85
- The trap is that both of those *worked*, for months, because the page is what
86
- anybody building a component puts it on. It fails on a card, a drawer, a toast,
87
- a panel or a photograph, and none of those is where a component gets built.
83
+ `Timeline`'s marker had this mistake. It was painted the page color to hide the
84
+ rail behind it, so it came out darker than any surface other than the page. On a
85
+ card, it showed as a black disc. The sparkline's ring terminus had the same
86
+ mistake and looked the same.
88
87
 
89
- **What to do instead:** put the color behind a hook that defaults to the page,
90
- so a consumer whose component is not on the page can correct it. That is what
91
- the library requires of itself, and the same reasoning applies to anything you
92
- build on top.
88
+ Both looked correct for months, because components are usually built on the
89
+ page. The mistake shows on a card, a drawer, a toast, a panel, or a photograph,
90
+ and components are not built on any of those.
91
+
92
+ Put the color behind a hook that defaults to the page color. When the component
93
+ sits on another surface, the hook can be set to match. Helix requires this of
94
+ its own components, and the same applies to anything you build with it.
93
95
 
94
96
  ## Contrast
95
97
 
96
- The theme engine measures an ink against the ground it is *really* painted on,
97
- which is why the ladder is a model rather than a naming convention: the engine
98
- has to know that `foreground` on `surface-overlay` is a real pair and
99
- `foreground-on-accent` on `surface` is not. See [Color](./color).
98
+ The theme engine measures each ink against the ground it is actually painted
99
+ on. The ladder records which pairs occur. For example, `foreground` on
100
+ `surface-overlay` is a real pair, and `foreground-on-accent` on `surface` is
101
+ not. See [Color](./color).
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Typed tags, and the framework that needs the least said about it.
2
+ description: The Svelte typings, and how Svelte passes props and binds events.
3
3
  parent: installation
4
4
  section: Getting started
5
5
  title: Svelte
@@ -11,35 +11,36 @@ title: Svelte
11
11
  import '@fusion.dev/helix/svelte'
12
12
  ```
13
13
 
14
- A types-only import, once. It augments `SvelteHTMLElements` so every Helix tag
15
- is typed in a component, and it does so by re-exporting the JSX types rather
16
- than emitting a second copy: Svelte binds a handler as `onhelix-before-change={…}`,
17
- which is the shape those types already describe, and two generated copies of one
18
- thing are two things that can disagree.
14
+ Add this types-only import once. It augments `SvelteHTMLElements`, so every
15
+ Helix tag is typed in a component. The import re-exports the JSX types. Svelte
16
+ binds a handler as `onhelix-before-change={…}`, which is the shape those types
17
+ already describe. With one shared copy of the types, the Svelte and JSX typings
18
+ cannot disagree.
19
19
 
20
- ## Nothing else to configure
20
+ ## Pass props and bind events
21
21
 
22
22
  ```svelte
23
23
  <helix-input label="Name" onchange={(event) => (name = event.target.value)} value={name} />
24
24
  ```
25
25
 
26
- Svelte sets a property when the element has one and an attribute otherwise, and
27
- binds `on*` with `addEventListener`. There is no unknown-element warning to
28
- silence and no compiler flag to set. That is the whole page, and it is short
29
- because Svelte was already right about custom elements.
26
+ Svelte sets a property when the element has one, and an attribute otherwise. It
27
+ binds `on*` with `addEventListener`. Svelte shows no unknown-element warning,
28
+ and you do not need a compiler flag.
30
29
 
31
- **Pass objects and arrays as properties**, which Svelte does on its own:
30
+ Svelte passes objects and arrays as properties on its own:
32
31
 
33
32
  ```svelte
34
33
  <helix-tabs items={tabs} />
35
34
  ```
36
35
 
37
- ## Two things that are not framework-specific
36
+ ## Read values and refuse changes
38
37
 
39
- `bind:` does not work on a Helix control, because there is no two-way binding
40
- convention a custom element can opt into. Read the value off the event.
38
+ These two rules apply in every framework.
41
39
 
42
- And a declared `value`, `open`, `checked` or `active` is a seed: the element
43
- starts there and goes on applying its own changes. To refuse one, cancel the
44
- matching `helix-before-*` event; withholding a state update refuses nothing.
45
- See [State and control](./state-and-control).
40
+ `bind:` does not work on a Helix control, because custom elements have no
41
+ two-way binding convention to opt into. Read the value from the event.
42
+
43
+ A declared `value`, `open`, `checked`, or `active` is a seed. The element starts
44
+ with that value and then applies its own changes. To refuse a change, cancel the
45
+ matching `helix-before-*` event. Skipping a state update does not refuse it. See
46
+ [State and control](./state-and-control).
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: The tool that turns intent inputs into a token set you can see.
2
+ description: Enter your theme inputs and see every token Helix computes from them.
3
3
  section: Tooling
4
4
  title: Theme builder
5
5
  ---
@@ -8,37 +8,47 @@ title: Theme builder
8
8
 
9
9
  [Open the theme builder](/helix/theme).
10
10
 
11
- Move a slider and every surface in the library resolves around it. A theme here
12
- is derived rather than picked, and this is where you see that happen.
11
+ When you move a slider, every component in the preview updates. Helix computes
12
+ a theme from a few inputs, and the theme builder shows you the result.
13
13
 
14
- ## What it does
14
+ ## What the theme builder shows
15
15
 
16
- You give it the intent inputs, the same ones `derive()` takes: a brand color, a
17
- chroma ceiling, a radius, a density, a contrast target, a motion character. It
18
- runs the real derivation, not an approximation of it, and shows you the complete
19
- token set plus the library rendered in it.
16
+ You enter the same inputs that `derive()` takes:
20
17
 
21
- Light and dark at once, because `[data-color-mode]` works on any element and the
22
- two previews are two subtrees of one document. See [Color modes](./color-modes).
18
+ - a brand color
19
+ - a chroma ceiling
20
+ - a radius
21
+ - a density
22
+ - a contrast target
23
+ - a motion character
24
+
25
+ The theme builder runs the full derivation. It shows you every token, and the
26
+ components drawn with them.
27
+
28
+ It shows light and dark at the same time. `[data-color-mode]` works on any
29
+ element, so the two previews are two subtrees of one document. See
30
+ [Color modes](./color-modes).
23
31
 
24
32
  ## The contrast column
25
33
 
26
- Every pairing the engine solved is listed with its measured ratio against the
27
- ground it is actually painted on. Three states:
34
+ The contrast column lists every color pair Helix computed. Each pair shows its
35
+ ratio, measured against the background it is painted on. A pair has one of
36
+ three states:
28
37
 
29
38
  - **Met.** The pair reaches the target.
30
- - **Unverifiable.** The ground is translucent, so there is no known color to
31
- measure against. Not a failure, and not a pass either: it is what a material
32
- choice costs you, reported rather than hidden.
33
- - **Pinned.** You supplied the color outright, so the engine did not solve for
34
- it and cannot promise anything about it.
39
+ - **Unverifiable.** The background is translucent, so there is no known color
40
+ to measure against. The pair neither passes nor fails. This is the cost of a
41
+ translucent material, and the theme builder reports it.
42
+ - **Pinned.** You set the color yourself. The engine did not compute it, so its
43
+ contrast is not guaranteed.
35
44
 
36
- A theme with a column of `unverifiable` is a theme making a trade. It should be
37
- possible to see that you made it.
45
+ A column of `unverifiable` pairs means your theme makes a trade. The column lets
46
+ you see that you made it.
38
47
 
39
- ## What comes out
48
+ ## What the theme builder exports
40
49
 
41
- The token block, as CSS, to paste into your own stylesheet:
50
+ The theme builder gives you the token block as CSS, to paste into your own
51
+ stylesheet:
42
52
 
43
53
  ```css
44
54
  :root {
@@ -47,20 +57,21 @@ The token block, as CSS, to paste into your own stylesheet:
47
57
  }
48
58
  ```
49
59
 
50
- Or the intent object, to keep in your repository and re-derive from:
60
+ It also gives you the intent object, to keep in your repository and derive the
61
+ theme from again:
51
62
 
52
63
  ```ts
53
64
  export const theme = { brand: '#2563eb', chroma: { ceiling: 0.1 }, radius: 2 }
54
65
  ```
55
66
 
56
- **Keep the intent, not the tokens.** The tokens are the output of a solver, and
57
- the solver improves: a theme stored as its inputs picks up a better derivation,
58
- and a theme stored as literal values is frozen at the version that produced
59
- it. The tokens are for pasting into a system that cannot run the engine.
67
+ **Keep the intent object in your repository.** The tokens are the output of a
68
+ solver, and the solver improves. A theme stored as its inputs gets each
69
+ improvement. A theme stored as token values stays at the version that produced
70
+ it. Use the tokens only in a system that cannot run the engine.
60
71
 
61
- ## What it does not do
72
+ ## Limits of the theme builder
62
73
 
63
- It shows you numbers and swatches, not components in your product. A contrast
64
- matrix is a grid with three states, which a terminal or a table renders better
65
- than a picture does, and the components beside it are there to sanity-check the
66
- result rather than to be a design surface.
74
+ The theme builder shows numbers and swatches. It does not show components in
75
+ your product. The contrast results are a grid with three states, and a terminal
76
+ or a table shows that grid more clearly than a picture. The components beside
77
+ it are for checking the result. They are not a design tool.