@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.
- package/LICENSE +21 -0
- package/content/accessibility.mdx +117 -0
- package/content/angular.mdx +60 -0
- package/content/browser-support.mdx +83 -0
- package/content/building-on-the-headless-tier.mdx +143 -0
- package/content/charting.mdx +93 -0
- package/content/coding-agents.mdx +139 -0
- package/content/color-modes.mdx +87 -0
- package/content/color.mdx +92 -0
- package/content/composition.mdx +171 -0
- package/content/elevation-and-material.mdx +93 -0
- package/content/events.mdx +125 -0
- package/content/forms.mdx +170 -0
- package/content/iconography.mdx +80 -0
- package/content/installation.mdx +125 -0
- package/content/intent-reference.mdx +107 -0
- package/content/introduction.mdx +69 -0
- package/content/localization.mdx +95 -0
- package/content/metrics.mdx +399 -0
- package/content/motion.mdx +91 -0
- package/content/other-frameworks.mdx +70 -0
- package/content/overlays.mdx +102 -0
- package/content/overriding-a-component.mdx +160 -0
- package/content/react.mdx +144 -0
- package/content/recipes.mdx +131 -0
- package/content/routing.mdx +208 -0
- package/content/sandbox.mdx +76 -0
- package/content/server-rendering.mdx +103 -0
- package/content/shape-and-borders.mdx +80 -0
- package/content/size-and-density.mdx +86 -0
- package/content/space-and-size.mdx +89 -0
- package/content/state-and-control.mdx +132 -0
- package/content/surfaces-and-separation.mdx +99 -0
- package/content/svelte.mdx +45 -0
- package/content/theme-builder.mdx +66 -0
- package/content/theming.mdx +111 -0
- package/content/tiers.mdx +159 -0
- package/content/tokens.mdx +88 -0
- package/content/typography.mdx +117 -0
- package/content/using-helix-with-an-agent.mdx +231 -0
- package/content/using-the-elements.mdx +125 -0
- package/content/versioning.mdx +72 -0
- package/content/visual-axes.mdx +75 -0
- package/content/vue.mdx +80 -0
- package/content/without-a-bundler.mdx +119 -0
- package/dist/catalog.d.ts +186 -0
- package/dist/catalog.js +296 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/naming.d.ts +14 -0
- package/dist/naming.js +28 -0
- package/dist/pages.generated.d.ts +7 -0
- package/dist/pages.generated.js +284 -0
- package/dist/reference.d.ts +105 -0
- package/dist/reference.js +181 -0
- package/dist/registry.d.ts +89 -0
- package/dist/registry.js +70 -0
- package/dist/sections.d.ts +44 -0
- package/dist/sections.js +36 -0
- package/dist/sources.generated.d.ts +6 -0
- package/dist/sources.generated.js +5158 -0
- 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.
|