@fusion.dev/helix-docs 0.0.0-dev

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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/content/accessibility.mdx +117 -0
  3. package/content/angular.mdx +60 -0
  4. package/content/browser-support.mdx +83 -0
  5. package/content/building-on-the-headless-tier.mdx +143 -0
  6. package/content/charting.mdx +93 -0
  7. package/content/coding-agents.mdx +139 -0
  8. package/content/color-modes.mdx +87 -0
  9. package/content/color.mdx +92 -0
  10. package/content/composition.mdx +171 -0
  11. package/content/elevation-and-material.mdx +93 -0
  12. package/content/events.mdx +125 -0
  13. package/content/forms.mdx +170 -0
  14. package/content/iconography.mdx +80 -0
  15. package/content/installation.mdx +125 -0
  16. package/content/intent-reference.mdx +107 -0
  17. package/content/introduction.mdx +69 -0
  18. package/content/localization.mdx +95 -0
  19. package/content/metrics.mdx +399 -0
  20. package/content/motion.mdx +91 -0
  21. package/content/other-frameworks.mdx +70 -0
  22. package/content/overlays.mdx +102 -0
  23. package/content/overriding-a-component.mdx +160 -0
  24. package/content/react.mdx +144 -0
  25. package/content/recipes.mdx +131 -0
  26. package/content/routing.mdx +208 -0
  27. package/content/sandbox.mdx +76 -0
  28. package/content/server-rendering.mdx +103 -0
  29. package/content/shape-and-borders.mdx +80 -0
  30. package/content/size-and-density.mdx +86 -0
  31. package/content/space-and-size.mdx +89 -0
  32. package/content/state-and-control.mdx +132 -0
  33. package/content/surfaces-and-separation.mdx +99 -0
  34. package/content/svelte.mdx +45 -0
  35. package/content/theme-builder.mdx +66 -0
  36. package/content/theming.mdx +111 -0
  37. package/content/tiers.mdx +159 -0
  38. package/content/tokens.mdx +88 -0
  39. package/content/typography.mdx +117 -0
  40. package/content/using-helix-with-an-agent.mdx +231 -0
  41. package/content/using-the-elements.mdx +125 -0
  42. package/content/versioning.mdx +72 -0
  43. package/content/visual-axes.mdx +75 -0
  44. package/content/vue.mdx +80 -0
  45. package/content/without-a-bundler.mdx +119 -0
  46. package/dist/catalog.d.ts +186 -0
  47. package/dist/catalog.js +296 -0
  48. package/dist/index.d.ts +7 -0
  49. package/dist/index.js +6 -0
  50. package/dist/naming.d.ts +14 -0
  51. package/dist/naming.js +28 -0
  52. package/dist/pages.generated.d.ts +7 -0
  53. package/dist/pages.generated.js +284 -0
  54. package/dist/reference.d.ts +105 -0
  55. package/dist/reference.js +181 -0
  56. package/dist/registry.d.ts +89 -0
  57. package/dist/registry.js +70 -0
  58. package/dist/sections.d.ts +44 -0
  59. package/dist/sections.js +36 -0
  60. package/dist/sources.generated.d.ts +6 -0
  61. package/dist/sources.generated.js +5158 -0
  62. package/package.json +46 -0
@@ -0,0 +1,45 @@
1
+ ---
2
+ description: Typed tags, and the framework that needs the least said about it.
3
+ parent: installation
4
+ section: Getting started
5
+ title: Svelte
6
+ ---
7
+
8
+ # Svelte
9
+
10
+ ```ts
11
+ import '@fusion.dev/helix/svelte'
12
+ ```
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.
19
+
20
+ ## Nothing else to configure
21
+
22
+ ```svelte
23
+ <helix-input label="Name" onchange={(event) => (name = event.target.value)} value={name} />
24
+ ```
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.
30
+
31
+ **Pass objects and arrays as properties**, which Svelte does on its own:
32
+
33
+ ```svelte
34
+ <helix-tabs items={tabs} />
35
+ ```
36
+
37
+ ## Two things that are not framework-specific
38
+
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.
41
+
42
+ And a declared `value`, `open`, `checked` or `active` is a claim of ownership:
43
+ the element announces and waits rather than applying the change itself. Use
44
+ `default-value` and its siblings when you want the element to own its own state.
45
+ See [State and control](./state-and-control).
@@ -0,0 +1,66 @@
1
+ ---
2
+ description: The tool that turns intent inputs into a token set you can see.
3
+ section: Tooling
4
+ title: Theme builder
5
+ ---
6
+
7
+ # Theme builder
8
+
9
+ [Open the theme builder](/helix/theme).
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.
13
+
14
+ ## What it does
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.
20
+
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).
23
+
24
+ ## The contrast column
25
+
26
+ Every pairing the engine solved is listed with its measured ratio against the
27
+ ground it is actually painted on. Three states:
28
+
29
+ - **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.
35
+
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.
38
+
39
+ ## What comes out
40
+
41
+ The token block, as CSS, to paste into your own stylesheet:
42
+
43
+ ```css
44
+ :root {
45
+ --helix-color-primary: oklch(…);
46
+ …
47
+ }
48
+ ```
49
+
50
+ Or the intent object, to keep in your repository and re-derive from:
51
+
52
+ ```ts
53
+ export const theme = { brand: '#2563eb', chroma: { ceiling: 0.1 }, radius: 2 }
54
+ ```
55
+
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 176 literal values is frozen at the version that produced
59
+ it. The tokens are for pasting into a system that cannot run the engine.
60
+
61
+ ## What it does not do
62
+
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.
@@ -0,0 +1,111 @@
1
+ ---
2
+ description: Deriving an accessible token set from intent inputs.
3
+ section: Theming
4
+ title: Deriving a theme
5
+ ---
6
+
7
+ # Theming
8
+
9
+ **Tokens are a function, not a table.**
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.
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.
18
+
19
+ ## Two ways to theme
20
+
21
+ They are both real and they are not the same thing, so they have different
22
+ names.
23
+
24
+ **Tuned** is the CSS path. Write a seed, the stylesheet re-derives, and nothing
25
+ has to be installed or run:
26
+
27
+ ```css
28
+ :root {
29
+ --helix-seed-brand: oklch(0.58 0.19 20);
30
+ --helix-seed-accent-hue: 140;
31
+ --helix-seed-chroma-scale: 0.4;
32
+ }
33
+ ```
34
+
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.
40
+
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.
45
+
46
+ **Verified** is the engine. It solves rather than steps, measures every pair it
47
+ emits, and hands back an artifact:
48
+
49
+ ```ts
50
+ import { deriveTheme, toCss } from '@fusion.dev/helix-theme'
51
+
52
+ const theme = deriveTheme({
53
+ brand: '#0b3d91',
54
+ contrastTarget: 4.5, // 4.5 is WCAG AA, 7 is AAA
55
+ scheme: 'light',
56
+ })
57
+
58
+ if (theme.violations.length > 0) {
59
+ throw new Error(theme.violations.join('\n'))
60
+ }
61
+
62
+ console.log(toCss(theme))
63
+ ```
64
+
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 colours 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.
69
+
70
+ Reach for tuned to try a color. Reach for verified to ship one.
71
+
72
+ ## Why OKLCH
73
+
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.
77
+
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.
80
+
81
+ ## What it protects against
82
+
83
+ Both of these produce a palette that passes review and fails in production.
84
+ Both are covered by regression tests.
85
+
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.
91
+
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.
97
+
98
+ ## Inspecting a theme
99
+
100
+ Every derived token carries the check that produced it:
101
+
102
+ ```ts
103
+ const theme = deriveTheme({ brand })
104
+
105
+ for (const token of theme.tokens) {
106
+ console.log(token.name, token.hex, token.contrast, token.chromaReduced)
107
+ }
108
+ ```
109
+
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.
@@ -0,0 +1,159 @@
1
+ ---
2
+ description: Why the headless and styled tiers are separate packages.
3
+ order: 2
4
+ section: Getting started
5
+ title: Tiers
6
+ ---
7
+
8
+ # Tiers
9
+
10
+ Helix ships as four stacked packages. Each has one job, and the dependency
11
+ direction never reverses.
12
+
13
+ | Package | Owns | Depends on |
14
+ | --- | --- | --- |
15
+ | `@fusion.dev/helix-styles` | Tokens and the base layer. Pure CSS | nothing |
16
+ | `@fusion.dev/helix-core` | Element base class, mixins, decorators | Lit |
17
+ | `@fusion.dev/helix-headless` | Structure, accessibility, state, form participation | `core` |
18
+ | `@fusion.dev/helix` | Presentation: tokens, parts, and the visual axes | `headless`, `styles` |
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).
25
+
26
+ ## Why the split
27
+
28
+ **`headless` must never depend on `styles`.**
29
+
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.
34
+
35
+ ## What belongs where
36
+
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.
40
+
41
+ The styled tier extends it and adds only presentation. Token references live
42
+ here.
43
+
44
+ ```ts
45
+ // headless/src/accordion/item/accordion-item.ts
46
+ // no CSS, no tokens
47
+ export class HelixHeadlessAccordionItem extends MixinControllerOpen(HelixElement) {
48
+ protected override render() {
49
+ return html`
50
+ <div part="container">
51
+ <button aria-expanded=${this.open} part="trigger" @click=${this.handleToggle}>
52
+ <slot name="summary"></slot>
53
+ </button>
54
+ </div>
55
+ `
56
+ }
57
+ }
58
+
59
+ // web/src/accordion/root/accordion-root.ts
60
+ // extends the headless one, adds presentation only
61
+ import { styles } from './accordion-root.css.js'
62
+
63
+ @customElement('helix-accordion')
64
+ export class HelixAccordion extends HelixHeadlessAccordion {
65
+ @property({ reflect: true })
66
+ separation?: 'outlined' | 'plain'
67
+
68
+ static override styles: CSSResultGroup = styles
69
+ }
70
+ ```
71
+
72
+ ```css
73
+ /* web/src/accordion/item/accordion-item.css */
74
+ [part='trigger'] {
75
+ min-block-size: var(--helix-size-control);
76
+ padding: var(--helix-space-sm);
77
+ }
78
+ ```
79
+
80
+ ## Styling from the outside
81
+
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:
84
+
85
+ ```css
86
+ helix-accordion-item::part(trigger) {
87
+ font-weight: 600;
88
+ }
89
+ ```
90
+
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:
94
+
95
+ ```css
96
+ helix-accordion {
97
+ --accordion-radius: 1rem;
98
+ }
99
+ ```
100
+
101
+ ## The window before an element upgrades
102
+
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.
108
+
109
+ `@fusion.dev/helix` closes it with `elements.css`, which hides every one of its
110
+ tags until the definition lands:
111
+
112
+ ```css
113
+ helix-card:not(:defined) {
114
+ visibility: hidden;
115
+ }
116
+ ```
117
+
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.
120
+
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.
126
+
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.
130
+
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:
134
+
135
+ ```css
136
+ helix-base-accordion:not(:defined),
137
+ helix-base-dialog:not(:defined) {
138
+ visibility: hidden;
139
+ }
140
+ ```
141
+
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.
146
+
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.
151
+
152
+ ## Mixins and tooling
153
+
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.
@@ -0,0 +1,88 @@
1
+ ---
2
+ description: The two layers, the surface ladder, and what is enforced.
3
+ pinned: true
4
+ section: Theming
5
+ title: Tokens
6
+ ---
7
+
8
+ # Tokens
9
+
10
+ Two layers. You see the second.
11
+
12
+ | Layer | Shape | Who may use it |
13
+ |---|---|---|
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.
31
+
32
+ - **`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, which is why one token tried to do both jobs for a while. In
46
+ light they want opposite fills: a hovered row must be darker than a white page,
47
+ and a thumb must be lighter than the groove it sits in. If something looks
48
+ almost right in dark and wrong in light, this is usually why.
49
+
50
+ ## Per-component hooks
51
+
52
+ ```css
53
+ helix-card {
54
+ --card-padding: var(--helix-space-lg);
55
+ }
56
+ ```
57
+
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.
61
+
62
+ ## What is enforced
63
+
64
+ Four things fail the build:
65
+
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.
70
+
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.
74
+
75
+ ## Theming
76
+
77
+ Both modes live in one declaration:
78
+
79
+ ```css
80
+ --helix-color-surface: light-dark(var(--_helix-neutral-50), var(--_helix-neutral-900));
81
+ ```
82
+
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.
87
+
88
+ Absent, it follows the operating system.
@@ -0,0 +1,117 @@
1
+ ---
2
+ description: Three families, one scale, and a heading that is a real heading.
3
+ section: Foundations
4
+ title: Typography
5
+ ---
6
+
7
+ # Typography
8
+
9
+ ## The families
10
+
11
+ [//]: # (tokens:font)
12
+
13
+ | Token | Value | For |
14
+ | --- | --- | --- |
15
+ | `--helix-font-sans` | `ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif` | Everything, unless it is one of the two below. |
16
+ | `--helix-font-mono` | `ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace` | Code, and anything that has to line up in a column. |
17
+ | `--helix-font-display` | `ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif` | Headings meant to be looked at rather than read. Falls back to the sans face. |
18
+
19
+ [//]: # (/tokens)
20
+
21
+ Three, not a scale of weights and widths, because a design system with more type
22
+ families than a product has uses ends up with two that mean the same thing.
23
+
24
+ ## The scale
25
+
26
+ `--helix-text-xs` through `--helix-text-3xl`, plus `--helix-text-control`, which
27
+ is the size a control's own text takes and follows the region's density rather
28
+ than the type scale.
29
+
30
+ **The scale is applied to real headings by the base layer, not by a component.**
31
+
32
+ ```css
33
+ :where([data-prose] h1) { font-size: var(--helix-text-3xl); }
34
+ :where([data-prose] h2) { font-size: var(--helix-text-2xl); }
35
+ ```
36
+
37
+ Styling the element rather than a component means the markup and the appearance
38
+ cannot disagree by default, and it reaches prose the design system never renders:
39
+ anything out of a CMS, a markdown file, or a rich-text field lands inside
40
+ `[data-prose]` and comes out right without being rewritten.
41
+
42
+ **`Text` exists for the case where they genuinely have to come apart.** A
43
+ section's second heading sometimes has to look smaller without lying about the
44
+ outline, and a `<p>` sometimes has to read as a caption, so `as` chooses the
45
+ element and `variant` chooses the ramp:
46
+
47
+ ```html
48
+ <helix-text
49
+ as="h3"
50
+ variant="body"
51
+ >Reads as body, outlines as a heading</helix-text>
52
+ ```
53
+
54
+ They default to matching, so `as="h2"` and nothing else is the common case, and
55
+ the pairing is resolved in CSS rather than by writing an attribute onto the host,
56
+ because that would be a hydration mismatch in every React consumer. Size, weight
57
+ and leading are not properties: a ramp is a decision the system already made, and
58
+ exposing the parts offers a few hundred combinations of which a handful are
59
+ right. There is no separate `Heading` component, because that is `as`.
60
+
61
+ ## Applying it to your own text
62
+
63
+ ```html
64
+ <article data-prose>
65
+ <h2>A real heading</h2>
66
+ <p>Body text, at the right measure and leading.</p>
67
+ </article>
68
+ ```
69
+
70
+ `[data-prose]` is the container. Inside it, headings, paragraphs, lists, links
71
+ and code are all styled from the token layer.
72
+
73
+ For one-off text outside prose, `[data-text]` carries the ramp:
74
+
75
+ ```html
76
+ <span data-text="sm">Fine print</span>
77
+ ```
78
+
79
+ ## The vertical metrics
80
+
81
+ [//]: # (tokens:leading)
82
+
83
+ | Token | Value | For |
84
+ | --- | --- | --- |
85
+ | `--helix-leading-tight` | `1.2` | Body text set close, for a short measure. |
86
+ | `--helix-leading-normal` | `1.5` | Body text, and the default a paragraph takes. |
87
+ | `--helix-leading-loose` | `1.7` | Body text set open, for a long measure or a small size. |
88
+ | `--helix-leading-heading` | `calc(0.99em + 0.5rem)` | Headings, which want less than a paragraph. Written as a length plus an em so it tightens as the type grows. |
89
+
90
+ [//]: # (/tokens)
91
+
92
+ [//]: # (tokens:tracking)
93
+
94
+ | Token | Value | For |
95
+ | --- | --- | --- |
96
+ | `--helix-tracking-tight` | `-0.01em` | Large type, which needs the letters closer than a paragraph does. |
97
+ | `--helix-tracking-normal` | `0` | Body text, and the default. |
98
+ | `--helix-tracking-wide` | `0.02em` | Small capitals and labels, which need the room. |
99
+ | `--helix-tracking-wider` | `0.12em` | An uppercase eyebrow, which needs more room than a label. |
100
+
101
+ [//]: # (/tokens)
102
+
103
+ [//]: # (tokens:weight)
104
+
105
+ | Token | Value | For |
106
+ | --- | --- | --- |
107
+ | `--helix-weight-regular` | `400` | Body text, and the weight most of a page is set in. |
108
+ | `--helix-weight-medium` | `500` | A label, or a heading that should not shout. |
109
+ | `--helix-weight-semibold` | `600` | A heading, and the heaviest this system goes. |
110
+
111
+ [//]: # (/tokens)
112
+
113
+ **Headings have their own leading token rather than a smaller number of the body
114
+ one.** A heading set at the paragraph's line height reads as loose at every
115
+ size, and the correction is not proportional: the larger the type, the less
116
+ leading it wants. One token expresses that once instead of a magic multiplier at
117
+ every heading level.