@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,24 +1,23 @@
1
1
  ---
2
- description: What renders without a DOM, and what has to wait for one.
2
+ description: How components render on a server with no DOM, and what waits for the browser.
3
3
  section: Essentials
4
4
  title: Server rendering
5
5
  ---
6
6
 
7
7
  # Server rendering
8
8
 
9
- Every component renders on a server with no DOM. If you are only consuming
10
- Helix, the practical consequence is small: components work in your server
11
- components and there is nothing to configure. If you are extending one, the
12
- rules below are the whole contract.
9
+ Every component renders on a server with no DOM. If you only use Helix
10
+ components, they work in your server components with no configuration. If you
11
+ extend a component, follow the rules below. They are the complete set.
13
12
 
14
- ## There is no DOM
13
+ ## The server has no DOM
15
14
 
16
- Lit's element shim carries attributes. It does not carry a tree. `querySelector`
17
- is undefined, `this.children` is undefined, and anything that walks the DOM
18
- throws while rendering.
15
+ Lit's element shim has attributes and no element tree. `querySelector` and
16
+ `this.children` are undefined, and any code that walks the DOM throws during
17
+ rendering.
19
18
 
20
- Two real examples from this library, both found the first time their component
21
- was given a server spec:
19
+ We found these two examples in Helix the first time each component got a server
20
+ spec:
22
21
 
23
22
  ```ts
24
23
  // Select. `display` is read during render and reached the render root.
@@ -32,70 +31,71 @@ get hasSubmenu(): boolean {
32
31
  }
33
32
  ```
34
33
 
35
- Before those two guards, neither component could be server-rendered at all. Both
36
- looked correct in every browser, because the client always has a root and always
37
- has a tree.
34
+ Before we added those two guards, neither component could render on a server.
35
+ Both worked in every browser, because a browser always has a render root and an
36
+ element tree.
38
37
 
39
- **Anything that inspects the tree reads it through `this.knownChildren`**, in
40
- `willUpdate` or in a getter the render calls. It is every child on a client and
41
- none on a server, so the server renders "not known yet" and the client's first
42
- render already has the answer. Reading after the render instead means two
43
- renders on every client, which Lit's dev build warns about.
38
+ **Read the element tree through `this.knownChildren`**, in `willUpdate` or in a
39
+ getter that the render calls. On a client it holds every child, and on a server
40
+ it holds none. The server renders the "not known yet" state, and the client's
41
+ first render already has the children. If you read the tree after the render,
42
+ every client renders twice, and Lit's dev build warns about that.
44
43
 
45
- Not known is not empty. `this.childrenKnown` is false on a server, and a list
46
- there doesn't render its "Nothing to show" over items it can't see.
44
+ `this.childrenKnown` is false on a server, where the children cannot be read. A
45
+ list on a server therefore does not render its "Nothing to show" message over
46
+ items it cannot see.
47
47
 
48
48
  ## Declarative shadow DOM
49
49
 
50
- If you render with Lit SSR, its declarative shadow DOM hydrates. Load Lit's
51
- hydration support before the components:
50
+ If you render with Lit SSR, the declarative shadow DOM it writes hydrates in the
51
+ browser. Load Lit's hydration support before the components:
52
52
 
53
53
  ```ts
54
54
  import '@lit-labs/ssr-client/lit-element-hydrate-support.js'
55
55
  import '@fusion.dev/helix'
56
56
  ```
57
57
 
58
- Hydration assumes the markup already shows the client's first render. A value
59
- that differs is never written, so the page keeps the server's, and a template
60
- that differs throws. So while hydrating, a component renders exactly what the
61
- server did, and anything it could only learn in a browser waits for the update
62
- after:
58
+ Hydration assumes the markup already matches the client's first render. If a
59
+ value differs, hydration does not write it, and the page keeps the server's
60
+ value. If a template differs, hydration throws. While hydrating, a component
61
+ renders exactly what the server rendered. Anything it can read only in a browser
62
+ waits for the next update:
63
63
 
64
64
  - `this.hydrating` is true for the render that hydrates.
65
65
  - `this.knownChildren` is empty during it, as on the server.
66
- - `this.finishHydration()`, called from `firstUpdated`, asks for the update that
67
- reads what was skipped.
66
+ - `this.finishHydration()`, called from `firstUpdated`, requests the update
67
+ that reads what was skipped.
68
68
 
69
- Two things the platform does before any script can listen, and how components
70
- answer them:
69
+ The browser does two things before any script can listen. Components handle
70
+ them this way:
71
71
 
72
72
  - **A declarative shadow root reports its slots as it is parsed.** A component
73
- never learns what's slotted from `slotchange` alone. It reads its children
74
- first and uses `slotchange` for later changes.
75
- - **The parser fetches images.** A `load` or `error` can fire before hydration,
76
- so a component asks the image what happened (`this.serverRendered`) and relays
77
- the event a task later.
73
+ does not rely on `slotchange` alone to find what is slotted. It reads its
74
+ children first and uses `slotchange` for later changes.
75
+ - **The parser fetches images.** A `load` or `error` event can fire before
76
+ hydration. A component checks the state of the image (`this.serverRendered`)
77
+ and dispatches the event again a task later.
78
78
 
79
- A component that assigns its slots by hand gets a root that can only assign by
80
- name, because HTML can't declare manual assignment. On such a root
81
- (`this.assignsByName`) it writes `slot` onto its children with `this.route()`,
82
- and only there.
79
+ HTML cannot declare manual slot assignment. A component that assigns its slots
80
+ by hand therefore gets a root that can assign only by name. On such a root
81
+ (`this.assignsByName`), and only there, the component writes `slot` onto its
82
+ children with `this.route()`.
83
83
 
84
- A page's own scripts should run after the definitions load. That's the order
85
- hydration expects: a property set before hydration is one the server never
86
- rendered with.
84
+ Run your page's own scripts after the definitions load. Hydration expects that
85
+ order. A property you set before hydration is one the server did not render
86
+ with.
87
87
 
88
88
  `bun run check:spec:dsd` renders every example with Lit SSR, hydrates it in
89
89
  Chromium, and compares it with the same example rendered on a client.
90
90
 
91
- React server components don't use any of this. Their output is replayed through
92
- DOM APIs, and DSD doesn't activate for nodes a script inserts. See
93
- [React](./react).
91
+ React server components do not use declarative shadow DOM. Their output is
92
+ replayed through DOM APIs, and declarative shadow DOM does not activate for
93
+ nodes that a script inserts. See [React](./react).
94
94
 
95
95
  ## Never initialize state in `connectedCallback`
96
96
 
97
- SSR does not call it. Derive from properties, or seed in `willUpdate`, which SSR
98
- does call.
97
+ SSR does not call `connectedCallback`. Derive state from properties, or set
98
+ initial state in `willUpdate`, which SSR calls.
99
99
 
100
100
  ## Guard generated ids
101
101
 
@@ -105,50 +105,61 @@ if (!this.id) {
105
105
  }
106
106
  ```
107
107
 
108
- The server writes the id into the HTML; the client sees it present and skips
109
- generating a new one, so both agree. Without the guard they disagree and the
110
- association breaks on hydration.
108
+ The server writes the id into the HTML. On the client, the id is already
109
+ present, so the component does not generate a new one, and both ids match.
110
+ Without the guard, the ids differ, and the association breaks on hydration.
111
111
 
112
- ## Semantics on the host, presentation in the shadow root
112
+ ## Put semantics on the host and presentation in the shadow root
113
113
 
114
114
  IDREFs do not cross shadow boundaries, so `aria-labelledby` inside a shadow root
115
- can never resolve to a light-DOM id. Use `ElementInternals`, which writes no
116
- attribute at all, so the host stays exactly as you wrote it and hydration has
117
- nothing to disagree about.
118
-
119
- **Labels are the case that catches people.** `<label for>` cannot reach into a
120
- shadow root either, which is why a field's label is rendered *inside* the
121
- control rather than by anything wrapping it, and why a wrapping "form field"
122
- component cannot own the label without handing it down as a property. It is also
123
- why `aria-label` on a host has to be forwarded rather than left where it was
124
- written. See [Composition](./composition).
125
-
126
- Where an id genuinely cannot reach, **element references can**, and they resolve
127
- outward: an element in a shadow root may point at one in the light DOM.
128
- `Listbox` uses that for `aria-activedescendant`, because its options are your
129
- elements and the element that takes focus is in its shadow root. Feature-test
130
- them: assigning to a property an engine does not implement is a no-op rather
131
- than an error.
132
-
133
- ## A child announces itself to its parent
115
+ cannot resolve to a light-DOM id. Use `ElementInternals`. It writes no
116
+ attributes, so the host stays exactly as you wrote it, and hydration finds no
117
+ difference.
118
+
119
+ Labels have the same problem. `<label for>` cannot reach into a shadow root
120
+ either. For that reason:
121
+
122
+ - A field's label is rendered *inside* the control, and no wrapping element
123
+ renders it.
124
+ - A wrapping "form field" component can own the label only by passing it down
125
+ as a property.
126
+ - `aria-label` on a host has to be forwarded from where you wrote it.
127
+
128
+ See [Composition](./composition).
129
+
130
+ Where an id cannot reach, use an element reference. Element references resolve
131
+ outward, so an element in a shadow root can point at an element in the light
132
+ DOM. `Listbox` uses one for `aria-activedescendant`, because its options are
133
+ your elements and the element that takes focus is in its shadow root. Test for
134
+ support before you use element references. Assigning to a property that an
135
+ engine does not implement does nothing and throws no error.
136
+
137
+ ## A child dispatches a ready event to its parent
134
138
 
135
139
  A parent cannot poll for a child that has not upgraded yet. The child dispatches
136
- a bubbling ready event and the parent listens.
140
+ a bubbling ready event, and the parent listens for it.
137
141
 
138
- ## Writing a server spec
142
+ ## Write a server spec
139
143
 
140
- Every component has a `*.ssr.spec.ts`, and there is one rule about them worth
141
- repeating, because it has produced a false pass twice:
144
+ Every component has a `*.ssr.spec.ts` file. Breaking one rule for these specs
145
+ has produced a false pass twice:
142
146
 
143
147
  **Never assert structure against the raw SSR string.** Declarative shadow DOM
144
- inlines the component's stylesheet into the very markup being asserted, and that
145
- stylesheet names every part, host attribute and token the component uses. So
146
- `toContain('part="media"')` passes on a *selector*, and
147
- `not.toContain('data-scrolled')` fails on a `:host([data-scrolled])` *rule*,
148
- both saying nothing about what rendered.
149
-
150
- Use the helpers: `withoutStyles`, `hasPart`, `partTag`, `hostTag`. And match
151
- attribute bindings with a pattern, since they render across several lines:
148
+ inlines the component's stylesheet into the markup you assert against. That
149
+ stylesheet names every part, host attribute, and token the component uses.
150
+ `toContain('part="media"')` can pass because of a *selector*, and
151
+ `not.toContain('data-scrolled')` can fail because of a `:host([data-scrolled])`
152
+ *rule*. Neither result tells you what rendered.
153
+
154
+ Use these helpers:
155
+
156
+ - `withoutStyles`
157
+ - `hasPart`
158
+ - `partTag`
159
+ - `hostTag`
160
+
161
+ Match attribute bindings with a pattern, because they render across several
162
+ lines:
152
163
 
153
164
  ```ts
154
165
  expect(markup).toMatch(/<a\s+part="button"/) // ✅
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Five radii, two widths, and the two tokens that are a policy rather than a value.
2
+ description: The corner radius and border width tokens, and how a theme changes them.
3
3
  section: Foundations
4
4
  title: Shape and borders
5
5
  ---
@@ -20,12 +20,11 @@ title: Shape and borders
20
20
 
21
21
  [//]: # (/tokens)
22
22
 
23
- `full` is `9999px` rather than `50%`, which matters for anything that is not
24
- square: a percentage radius on a wide element gives an ellipse, and a pill is
25
- what you wanted.
23
+ `full` is `9999px`, which gives a pill shape on an element that is not square.
24
+ A percentage radius such as `50%` gives an ellipse on a wide element.
26
25
 
27
- Rounding is the fastest single change to how a system feels, and it is one
28
- token, but it is the **seed** rather than one of the four above:
26
+ Changing the rounding is the quickest way to change how your interface looks.
27
+ You change it with one token, the seed:
29
28
 
30
29
  ```css
31
30
  :root {
@@ -33,25 +32,32 @@ token, but it is the **seed** rather than one of the four above:
33
32
  }
34
33
  ```
35
34
 
36
- That makes every card, dialog, input and button squarer at once, because `sm`,
37
- `md` and `lg` are all derived from it: they become 1px, 2px and 3px. The theme
38
- engine takes a `radius` intent input for exactly this reason. See [Deriving a
39
- theme](./theming).
35
+ This makes every card, dialog, input, and button squarer at once, because `sm`,
36
+ `md`, and `lg` are all computed from the seed. With this seed, they become 1px,
37
+ 2px, and 3px. The theme engine has a `radius` intent input for the same purpose.
38
+ See [Deriving a theme](./theming).
40
39
 
41
- **Setting `--helix-radii-md` instead moves only what reads `md`**, which is
42
- neither the buttons, the inputs and the dialogs (`sm`) nor the cards (`lg`).
43
- The four rungs are siblings derived from the seed, not a chain, so overriding
44
- one is how you change a single step on purpose rather than how you reshape a
45
- system.
40
+ Setting `--helix-radii-md` changes only what uses `md`. Buttons, inputs, and
41
+ dialogs use `sm`, and cards use `lg`, so none of them change. Each step is
42
+ computed from the seed on its own, so overriding one step changes only that
43
+ step. To change the rounding everywhere, set the seed.
46
44
 
47
- **Every floating panel takes `sm`.** A dialog, a drawer, a menu, a popover, a
48
- tooltip, a toast, and the panel any field opens under itself: all the same
49
- corner, whatever the panel's size. A radius is read against the corner beside it
50
- rather than against the area behind it.
45
+ **Every floating panel uses `sm`.** This includes:
51
46
 
52
- **A shape that is round rather than softened does not use these.** An avatar, a
53
- loader and a radio are circles because of what they are, so they say `50%`
54
- directly and are unaffected by a theme that squares everything else off.
47
+ - dialogs
48
+ - drawers
49
+ - menus
50
+ - popovers
51
+ - tooltips
52
+ - toasts
53
+ - the panel a field opens below itself
54
+
55
+ They all have the same corner, whatever their size. You compare a radius with
56
+ the corners next to it, so the size of the panel does not matter.
57
+
58
+ Circles do not use these tokens. An avatar, a loader, and a radio are circles by
59
+ design, so they set `50%` directly. A theme that squares off everything else
60
+ does not change them.
55
61
 
56
62
  ## Border widths
57
63
 
@@ -66,26 +72,27 @@ directly and are unaffected by a theme that squares everything else off.
66
72
 
67
73
  [//]: # (/tokens)
68
74
 
69
- The first two are values you pick. **The second two are a decision**, and they
70
- belong to [Surfaces and separation](./surfaces-and-separation): they are what a
71
- theme sets to zero to go shadow-first, or to a width to go border-first.
75
+ You pick from the first two. The last two are policy tokens, described in
76
+ [Surfaces and separation](./surfaces-and-separation). A theme sets them to zero
77
+ to go shadow-first, or to a width to go border-first.
72
78
 
73
- This distinction is why a component's border width sits behind a per-component
74
- hook rather than being written inline. A width written inline can be recolored
75
- by a theme and never removed, and a hardcoded `border: none` can never have one
76
- added back, so a surface themes perfectly in one direction and not at all in the
77
- other.
79
+ For this reason, each component reads its border width from its own hook. A
80
+ theme can recolor a width written inline but cannot remove it. A theme also
81
+ cannot add a border back to a component that sets `border: none`. In both
82
+ cases, the surface can be themed in one direction only.
78
83
 
79
- ## Corner radius on a group
84
+ ## Corners in a group of controls
80
85
 
81
- Adjacent controls in a group flatten the corners between them, so a button group
82
- reads as one shape rather than three:
86
+ Controls next to each other in a group have flat corners where they meet, so a
87
+ button group looks like one shape:
83
88
 
84
89
  ```css
85
90
  [data-button]:not(:first-child) { border-start-start-radius: 0; }
86
91
  ```
87
92
 
88
- The base layer does this for the recipes, and the components do it for
89
- themselves. If you build your own group: it is the inner corners that go flat,
90
- not the outer ones, and it is `border-start-start-radius` rather than
91
- `border-top-left-radius` so it follows the writing direction.
93
+ The base layer does this for the recipes, and each component does it for
94
+ itself. If you build your own group:
95
+
96
+ - Flatten the inner corners, and keep the outer corners rounded.
97
+ - Use `border-start-start-radius`, which follows the writing direction.
98
+ `border-top-left-radius` does not.
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Why no control has a size, and what to write instead.
2
+ description: Controls take their height from the density of the region they are in. Set the density once on the region.
3
3
  section: Theming
4
4
  title: Size and density
5
5
  ---
@@ -17,47 +17,46 @@ title: Size and density
17
17
  </helix-flex>
18
18
  ```
19
19
 
20
- There is no `size` prop on a button, an input, a select or a checkbox. That
21
- looks like a restriction and is not one.
20
+ Buttons, inputs, selects, and checkboxes have no `size` prop.
22
21
 
23
- ## Size does three jobs
22
+ ## What size is used for
24
23
 
25
- **Emphasis.** The `emphasis` axis carries it. A filled primary button says "this one
26
- matters" far louder than eight more pixels ever will.
24
+ Size does three jobs:
27
25
 
28
- **Fit.** This is a fact about the region rather than about the control. A
29
- toolbar is tight, a hero is spacious, a table row is tighter still. The control
30
- does not know which it is in; the region does.
26
+ - Emphasis shows which control matters most. The `emphasis` axis sets it. A
27
+ filled primary button marks the important action much more clearly than eight
28
+ more pixels of height.
29
+ - Fit depends on the region. A toolbar is tight, a hero is spacious, and a table
30
+ row is tighter still. The region sets the fit for every control in it.
31
+ - Optical role applies only to things that are not controls. An avatar at 24 in
32
+ a list and at 96 on a profile are two different pictures. That is why
33
+ `Avatar`, `Loader`, and `Orb` keep a `size`.
31
34
 
32
- **Optical role.** Real, and only for things that are not controls. An avatar at
33
- 24 in a list and 96 on a profile is a different graphic, not a taller control.
34
- That is why `size` survives on `Avatar`, `Loader` and `Orb`: those are
35
- pictures.
35
+ ## Can two controls differ in height?
36
36
 
37
- ## The test that settles it
37
+ Ask whether two controls placed together can ever differ in height:
38
38
 
39
- *Can two controls sitting together legitimately differ in height?*
39
+ - Save and Cancel
40
+ - An icon button beside a text button
41
+ - An input and its submit button
42
+ - The two halves of a split button
40
43
 
41
- Save and Cancel. An icon button beside a text button. An input and its submit.
42
- The two halves of a split button. In every case the answer is no.
44
+ In every case the answer is no.
43
45
 
44
- So a per-instance size is only ever used to say something about the **region**,
45
- one element at a time, which is exactly how a row ends up with one member the
46
- wrong height. Said once on the region, a ragged row is not discouraged. It is
47
- unwritable.
46
+ A size on each control describes the **region**, one element at a time, and
47
+ that is how a row ends up with one control at the wrong height. When you set the
48
+ density once on the region, you cannot write a row of mismatched heights.
48
49
 
49
- `Dialog` keeps a `size` by the same test read from the other end. Two dialogs
50
- never sit together: one is on screen at a time, in the top layer, so it *is* the
51
- region rather than a control in one.
50
+ `Dialog` keeps a `size` by the same test. Two dialogs never sit together. One is
51
+ on screen at a time, in the top layer, so the dialog is itself the region.
52
52
 
53
- That is also what makes this safe for anything generating code, whether that is
54
- a person or a model: there is no size to get wrong, and one question to ask per
55
- region instead of one per element.
53
+ This also helps when you or a model generate code. There is no size to get wrong
54
+ on each element, and you make one choice for each region.
56
55
 
57
- ## How to use it
56
+ ## Set the density of a region
58
57
 
59
- Put `data-density` on any container. `sm`, `md` or `lg`. `md` is the default and
60
- is never written.
58
+ Put `data-density` on any container, with a value of `sm`, `md`, or `lg`. `md`
59
+ is the default, so you never write it.
61
60
 
62
61
  ```html
63
62
  <helix-toolbar>
@@ -67,24 +66,30 @@ is never written.
67
66
  </helix-toolbar>
68
67
  ```
69
68
 
70
- **Containers that are dense by nature declare it themselves.** The app header,
71
- the toolbar and the announcement all do, so a control put in one is already the
72
- right height without anybody deciding.
69
+ Containers that are dense by design set their own density. The app header, the
70
+ toolbar, and the announcement all do, so a control inside one of them is already
71
+ the right height.
73
72
 
74
- It works through custom properties (`--helix-size-control`,
75
- `--helix-size-checkbox`, `--helix-size-switch`, `--helix-size-tag`), which is
76
- why it costs no component any API and crosses every shadow boundary on its own.
73
+ Density works through these custom properties:
77
74
 
78
- The `-sm` / `-md` / `-lg` tokens are the scale and never move. The unsuffixed
79
- ones are the current step, and `data-density` is what moves them.
75
+ - `--helix-size-control`
76
+ - `--helix-size-checkbox`
77
+ - `--helix-size-switch`
78
+ - `--helix-size-tag`
80
79
 
81
- ## What this is not
80
+ Because of this, density adds no API to any component, and it passes through
81
+ every shadow boundary.
82
82
 
83
- **`data-link-size` stays.** A text link has no control height: its size is
84
- typography, and it follows the text around it.
83
+ The `-sm`, `-md`, and `-lg` tokens are the scale, and they never change. The
84
+ unsuffixed tokens hold the current step, and `data-density` changes them.
85
85
 
86
- **A control may still declare its own scale.** `--helix-size-switch` is one step
87
- above `--helix-size-checkbox` at every density, because a switch is a track with
88
- a thumb inside it and a checkbox is a box with a mark in it. That is the
89
- component choosing where it sits on the scale, once, for everyone. It is not the
90
- same thing as a consumer setting a height per instance.
86
+ ## Exceptions
87
+
88
+ `data-link-size` still exists. A text link has no control height. Its size comes
89
+ from typography, and it follows the text around it.
90
+
91
+ A component can still set its own step on the scale. `--helix-size-switch` is
92
+ one step above `--helix-size-checkbox` at every density, because a switch is a
93
+ track with a thumb inside it and a checkbox is a box with a mark in it. The
94
+ component sets this once, and it applies to every instance. You do not set a
95
+ height on each instance.
@@ -1,14 +1,14 @@
1
1
  ---
2
- description: One spacing scale, and why a control's height is not on it.
2
+ description: Use the space scale for gaps and the size tokens for the height of controls.
3
3
  section: Foundations
4
4
  title: Space and size
5
5
  ---
6
6
 
7
7
  # Space and size
8
8
 
9
- Two scales that look similar and answer different questions. Space is the gaps
10
- between things. Size is how big a control is. Mixing them is how a layout ends
11
- up with a button that is one step of padding taller than the input beside it.
9
+ Helix has two scales. The space scale sets the gaps between things. The size
10
+ scale sets how big a control is. If you use one for the other, a button can end
11
+ up one step of padding taller than the input beside it.
12
12
 
13
13
  ## Space
14
14
 
@@ -29,15 +29,16 @@ up with a button that is one step of padding taller than the input beside it.
29
29
 
30
30
  [//]: # (/tokens)
31
31
 
32
- Nine steps, t-shirt named rather than numbered. A numbered scale invites
33
- arithmetic (`space-4` is twice `space-2`) which stops being true the moment the
34
- scale is tuned, and invites a `space-4.5` the first time something does not fit.
32
+ The scale has nine steps, named like t-shirt sizes. Numbered steps suggest
33
+ arithmetic, such as `space-4` being twice `space-2`. That stops being true as
34
+ soon as the scale is tuned, and it leads to a `space-4.5` the first time
35
+ something does not fit.
35
36
 
36
- `--helix-space-control` is the tenth and is different in kind: it is the inset a
37
- control uses, and it follows the region's density rather than sitting on the
37
+ `--helix-space-control` is a tenth token of a different kind. It is the inset a
38
+ control uses, and it follows the region's density. It is not a step on the
38
39
  scale. See [Size and density](./size-and-density).
39
40
 
40
- Use them for gap, padding and margin:
41
+ Use the space tokens for gap, padding, and margin:
41
42
 
42
43
  ```css
43
44
  .toolbar {
@@ -49,24 +50,31 @@ Use them for gap, padding and margin:
49
50
 
50
51
  ## Size
51
52
 
52
- Forty-one tokens, and almost all of them name a specific thing:
53
- `--helix-size-control`, `--helix-size-checkbox`, `--helix-size-switch`,
54
- `--helix-size-avatar`, `--helix-size-icon`, `--helix-size-tag`,
55
- `--helix-size-header`, `--helix-size-progress`, `--helix-size-meter`.
53
+ The size scale has forty-one tokens, and almost all of them name a specific
54
+ thing:
56
55
 
57
- Most come in a `-sm`, `-md` and `-lg` set beside the unsuffixed one. **The
58
- unsuffixed token is the live one** and the suffixed ones are the values the
59
- density axis picks between. The loader is the exception: it has `-sm`, `-md`
60
- and `-lg` and no unsuffixed token above them, because nothing reads a loader's
61
- size off the density. So a component reads `--helix-size-control` and the
62
- region's `[data-density]` decides which of `-sm`, `-md` or `-lg` that resolves
63
- to.
56
+ - `--helix-size-control`
57
+ - `--helix-size-checkbox`
58
+ - `--helix-size-switch`
59
+ - `--helix-size-avatar`
60
+ - `--helix-size-icon`
61
+ - `--helix-size-tag`
62
+ - `--helix-size-header`
63
+ - `--helix-size-progress`
64
+ - `--helix-size-meter`
64
65
 
65
- That is why a component has no `size` property: the answer is the region's, and
66
- saying it once there means a ragged row is not merely discouraged, it is
67
- unwritable.
66
+ Most come in a `-sm`, `-md`, and `-lg` set beside the unsuffixed token. **The
67
+ unsuffixed token is the live one.** The suffixed tokens are the values the
68
+ density axis picks between. A component reads `--helix-size-control`, and the
69
+ region's `[data-density]` sets whether that resolves to `-sm`, `-md`, or `-lg`.
68
70
 
69
- ## The focus ring is three tokens
71
+ The loader is the exception. It has `-sm`, `-md`, and `-lg` and no unsuffixed
72
+ token, because nothing reads a loader's size from the density.
73
+
74
+ This is why a component has no `size` property. The region sets the size once,
75
+ so you cannot write a row of controls with different heights.
76
+
77
+ ## Focus ring tokens
70
78
 
71
79
  ```
72
80
  --helix-size-focus-ring the ring's own thickness
@@ -74,16 +82,15 @@ unwritable.
74
82
  --helix-size-focus-reach ring plus offset, for reserving room
75
83
  ```
76
84
 
77
- The third exists because a ring drawn outside a control needs room, and every
78
- component that draws one otherwise recomputes the sum. Reserve with `reach`,
79
- draw with the other two.
85
+ A ring drawn outside a control needs room. `--helix-size-focus-reach` holds that
86
+ sum, so each component that draws a ring does not compute it again. Use `reach`
87
+ to reserve the room, and the other two to draw the ring.
80
88
 
81
89
  ## What not to do
82
90
 
83
- **Do not use a size token for a gap.** `--helix-size-control` is a height, and a
84
- layout that uses it as a gap changes meaning the day a density changes under it.
91
+ **Do not use a size token for a gap.** `--helix-size-control` is a height. A gap
92
+ that uses it changes when the region's density changes.
85
93
 
86
- **Do not use a raw value because the scale has no step for it.** If a gap is
87
- genuinely between `sm` and `md`, say so rather than writing `11px`: it is
88
- usually a sign the two things being separated belong at different levels of the
89
- layout.
94
+ **Do not use a raw value when the scale has no step for it.** If a gap really
95
+ falls between `sm` and `md`, note it and do not write `11px`. It usually means
96
+ the two things it separates belong at different levels of the layout.