@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,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: One attribute, two modes, and no token restated to honor it.
|
|
3
|
+
section: Foundations
|
|
4
|
+
title: Color modes
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Color modes
|
|
8
|
+
|
|
9
|
+
Light and dark are one attribute and no duplicated tokens. There is no second
|
|
10
|
+
stylesheet, no `.dark` class to toggle, and nothing to remember to update twice.
|
|
11
|
+
|
|
12
|
+
## How it works
|
|
13
|
+
|
|
14
|
+
```css
|
|
15
|
+
:root {
|
|
16
|
+
color-scheme: light dark;
|
|
17
|
+
--helix-color-background: light-dark(#fff, #0b0b0d);
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Every token is written once, with both halves in a `light-dark()`. Which half
|
|
22
|
+
resolves is decided by `color-scheme` and nothing else, so the mode is a single
|
|
23
|
+
inherited property and no token is restated to honor it. That is 761
|
|
24
|
+
declarations across the library, 135 of them in the token layer, expressing two
|
|
25
|
+
modes without a second copy of anything.
|
|
26
|
+
|
|
27
|
+
## Setting the mode
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<html data-color-mode="dark">
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`[data-color-mode="light"]` and `[data-color-mode="dark"]` set `color-scheme`,
|
|
34
|
+
and `color-scheme` inherits, so the attribute works **on any element** rather
|
|
35
|
+
than only on the root:
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<div data-color-mode="light">
|
|
39
|
+
<!-- A light preview inside a dark page. One attribute. -->
|
|
40
|
+
</div>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
That is the whole mechanism, and it is why the theme builder can show you a
|
|
44
|
+
light and a dark rendering of the same component side by side without rendering
|
|
45
|
+
two documents.
|
|
46
|
+
|
|
47
|
+
## Following the system
|
|
48
|
+
|
|
49
|
+
Unset, `:root` declares `color-scheme: light dark`, which means the operating
|
|
50
|
+
system decides. That is the default and it is usually right.
|
|
51
|
+
|
|
52
|
+
A `prefers-color-scheme` media query still exists in the token layer for one
|
|
53
|
+
narrow case rather than being the mechanism: a value that has to be *derived*
|
|
54
|
+
from the mode, rather than chosen between two, cannot be expressed as
|
|
55
|
+
`light-dark()` and has to ask which mode is current.
|
|
56
|
+
|
|
57
|
+
## The one component that inverts
|
|
58
|
+
|
|
59
|
+
`Toast` renders in the opposite mode to the page it is on, which is why it is
|
|
60
|
+
the exception written into the base layer:
|
|
61
|
+
|
|
62
|
+
```css
|
|
63
|
+
helix-toast:not([data-color-mode]) {
|
|
64
|
+
color-scheme: var(--_helix-mode-inverted);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
A toast is chrome over the page rather than part of it, and inverting is how it
|
|
69
|
+
reads as separate without a border. Opt out by putting `data-color-mode` on it,
|
|
70
|
+
which then wins, because the rule only applies to a toast that has not been
|
|
71
|
+
told.
|
|
72
|
+
|
|
73
|
+
## What this means for your own CSS
|
|
74
|
+
|
|
75
|
+
Write `light-dark()` for your own colours and set nothing else:
|
|
76
|
+
|
|
77
|
+
```css
|
|
78
|
+
.callout {
|
|
79
|
+
background: light-dark(#eef, #223);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
It will follow the page, follow a subtree that was told otherwise, and follow
|
|
84
|
+
the system when nothing has said. Do not write a `prefers-color-scheme` query
|
|
85
|
+
for it, and do not key off the `data-color-mode` attribute: both stop working
|
|
86
|
+
the moment a subtree overrides the mode, which is the case this design exists to
|
|
87
|
+
make cheap.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: The roles, the ramps, and why the palette is solved rather than picked.
|
|
3
|
+
section: Foundations
|
|
4
|
+
title: Color
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Color
|
|
8
|
+
|
|
9
|
+
Seventy-four of the library's 177 tokens are color, which is most of what a
|
|
10
|
+
theme is. They are roles rather than values: nothing in Helix is named after
|
|
11
|
+
what it looks like, because a name like `--brand-blue-500` stops being true the
|
|
12
|
+
moment anybody rethemes.
|
|
13
|
+
|
|
14
|
+
## The roles
|
|
15
|
+
|
|
16
|
+
**Grounds**, which things sit on. `background` is the page itself, and the
|
|
17
|
+
surfaces above it are a ladder rather than a set:
|
|
18
|
+
|
|
19
|
+
[//]: # (tokens:surface-ladder)
|
|
20
|
+
|
|
21
|
+
| Token | Sample | For |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `--helix-color-background` | <span data-token-swatch style={{ background: 'var(--helix-color-background)' }} /> | The page. Told apart by nothing, because it is the ground. |
|
|
24
|
+
| `--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. |
|
|
25
|
+
| `--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. |
|
|
26
|
+
| `--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. |
|
|
27
|
+
| `--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. |
|
|
28
|
+
|
|
29
|
+
[//]: # (/tokens)
|
|
30
|
+
|
|
31
|
+
Which one to reach for is a question about the geometry rather than about the
|
|
32
|
+
shade, and it is the thing people most often get wrong. See [Surfaces and
|
|
33
|
+
separation](./surfaces-and-separation).
|
|
34
|
+
|
|
35
|
+
**Inks**, which go on grounds. `foreground`, then `foreground-muted` and
|
|
36
|
+
`foreground-subtle` for text that recedes, and `foreground-on-accent` for text
|
|
37
|
+
that sits on a filled accent rather than on a surface.
|
|
38
|
+
|
|
39
|
+
**Intent**, which says what something means. `primary` and `accent` for
|
|
40
|
+
emphasis, each with `-hover`, `-active` and `-subtle`; `info`, `success`,
|
|
41
|
+
`warning` and `danger` for status, each with a `-surface` and a `-surface-hover`
|
|
42
|
+
for the tinted box the message sits in.
|
|
43
|
+
|
|
44
|
+
**Series**, twenty-four of them, for charts. Ordered rather than arbitrary: a
|
|
45
|
+
chart with three lines uses the first three, and they are chosen to stay
|
|
46
|
+
distinguishable in that order rather than to look good as a full set.
|
|
47
|
+
|
|
48
|
+
**Syntax**, eight roles for code: `callable`, `comment`, `constant`, `invalid`,
|
|
49
|
+
`keyword`, `name`, `string`, `tag`. Deliberately fewer than a full editor theme,
|
|
50
|
+
because a code block in documentation is read rather than edited.
|
|
51
|
+
|
|
52
|
+
## OKLCH
|
|
53
|
+
|
|
54
|
+
Every color is authored in OKLCH. Equal numeric steps in sRGB are not equal
|
|
55
|
+
perceptual steps, which makes both ramp generation and contrast arithmetic
|
|
56
|
+
unreliable: a ramp that is even by the numbers looks uneven, and a contrast
|
|
57
|
+
solver working in the wrong space cannot converge on a target it can measure.
|
|
58
|
+
|
|
59
|
+
Concretely, this is what lets the theme engine take a brand color and *solve*
|
|
60
|
+
for a palette rather than have someone pick one. See [Deriving a
|
|
61
|
+
theme](./theming).
|
|
62
|
+
|
|
63
|
+
## Contrast is a guarantee
|
|
64
|
+
|
|
65
|
+
The engine measures every ink against the ground it will really be painted on
|
|
66
|
+
and searches for the lightness that meets the target, rather than proposing a
|
|
67
|
+
value and checking it afterwards. Every pairing it produces meets the target by
|
|
68
|
+
construction.
|
|
69
|
+
|
|
70
|
+
That guarantee is only as good as the list of pairs somebody thought of, so two
|
|
71
|
+
things are checked rather than one. The engine's declared surface model is held
|
|
72
|
+
against what components actually paint, and then every pairing is measured on a
|
|
73
|
+
laid-out page, which is the only way to see the ones no stylesheet states: just
|
|
74
|
+
eleven rules in the library set an ink and a ground together, where 165 set only
|
|
75
|
+
an ink and 112 only a ground, and a pairing the cascade assembled is still a
|
|
76
|
+
pairing a reader has to be able to read.
|
|
77
|
+
|
|
78
|
+
## Do not reach past the semantic layer
|
|
79
|
+
|
|
80
|
+
```css
|
|
81
|
+
/* Yours to read. */
|
|
82
|
+
color: var(--helix-color-foreground-muted);
|
|
83
|
+
|
|
84
|
+
/* Not yours to read, and `check:tokens` fails the build on it. */
|
|
85
|
+
color: var(--_helix-neutral-500);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The primitive layer underneath, `--_helix-neutral-500` and its kind, is
|
|
89
|
+
`helix/styles`'s own business. The underscore marks it private and the
|
|
90
|
+
indirection is the whole point: it is what lets a theme change what
|
|
91
|
+
`--helix-color-primary` resolves to without every component knowing which step
|
|
92
|
+
of which ramp it happened to be.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Families, parts and groups, and which to reach for.
|
|
3
|
+
section: Essentials
|
|
4
|
+
title: Composition
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Composition
|
|
8
|
+
|
|
9
|
+
Helix has families, parts and groups, and they are not stylistic choices. Each
|
|
10
|
+
answers a different question, and knowing which you are looking at tells you
|
|
11
|
+
what to import and what to write.
|
|
12
|
+
|
|
13
|
+
## Namespaces mean composition
|
|
14
|
+
|
|
15
|
+
`Tabs.Root` contains `Tabs.Tab`. The namespace says *this goes inside that*.
|
|
16
|
+
|
|
17
|
+
It never says *this is a version of that*. An icon-only button is not a part of
|
|
18
|
+
`Button`, so `IconButton` is top-level. The rule falls out of one line:
|
|
19
|
+
|
|
20
|
+
> Variants are props. Alternatives are separate components.
|
|
21
|
+
|
|
22
|
+
A button that is filled instead of outlined is the same component with a
|
|
23
|
+
different `emphasis`. A button that holds only an icon has a different
|
|
24
|
+
accessible-name requirement and a different shape, so it is its own component.
|
|
25
|
+
|
|
26
|
+
## When a region becomes an element
|
|
27
|
+
|
|
28
|
+
When it has behavior, state or API. Otherwise it is a slot.
|
|
29
|
+
|
|
30
|
+
`Card`'s header, body and footer carried none of the three, so five elements
|
|
31
|
+
became one and a title became a real `<h2>` the consumer writes. `Tabs.Tab`,
|
|
32
|
+
`Select.Option` and `Accordion.Item` do carry state, so they stay elements.
|
|
33
|
+
|
|
34
|
+
**An empty region still costs layout.** A `<slot>` is `display: contents`, so an unassigned one generates no
|
|
35
|
+
box: no grid row, no flex item, no gap. A wrapper generates one either way.
|
|
36
|
+
Measured on Card with identical content: wrappers present but empty rendered
|
|
37
|
+
85px against an ideal of 53px. Bare slots rendered 53px with no hiding logic at
|
|
38
|
+
all.
|
|
39
|
+
|
|
40
|
+
## When a group is a part
|
|
41
|
+
|
|
42
|
+
The test is whether the group reports a form value.
|
|
43
|
+
|
|
44
|
+
**`Radio.Group` and `Toggle.Group` do.** They own the `name`, the `value` and
|
|
45
|
+
the validity, and a lone radio is not a control at all: it is an answer to one.
|
|
46
|
+
The group is what the family is *about*, so it is a part of it, and the member
|
|
47
|
+
has nowhere else to be.
|
|
48
|
+
|
|
49
|
+
**`Checkbox Group` and `Button Group` do not.** Neither calls `setFormValue`.
|
|
50
|
+
Each checkbox is form-associated and submits itself; the group adds
|
|
51
|
+
`role="group"`, a shared name, and one place to read the set. A button needs no
|
|
52
|
+
group at all. Both are their own families, because their members are complete
|
|
53
|
+
controls that the family does not need the group to explain.
|
|
54
|
+
|
|
55
|
+
A container holding mixed content is a family for the same reason. `Toolbar`
|
|
56
|
+
holds buttons, dropdowns and inputs, and is about none of them.
|
|
57
|
+
|
|
58
|
+
## A content property is its slot's fallback
|
|
59
|
+
|
|
60
|
+
Where a property carries content rather than configuration, it is the fallback
|
|
61
|
+
content of a slot of the same name, never a second thing rendered beside it.
|
|
62
|
+
|
|
63
|
+
```html
|
|
64
|
+
<!-- The property -->
|
|
65
|
+
<helix-tooltip content="Save"></helix-tooltip>
|
|
66
|
+
|
|
67
|
+
<!-- The slot, which replaces it -->
|
|
68
|
+
<helix-tooltip>
|
|
69
|
+
<strong>Save</strong>, or press <helix-kbd>⌘S</helix-kbd>
|
|
70
|
+
</helix-tooltip>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Text stays an attribute, because that is what most tooltips and most avatars
|
|
74
|
+
hold. Anything richer replaces it by being slotted. Both can never show at once,
|
|
75
|
+
and no component has to detect which you did.
|
|
76
|
+
|
|
77
|
+
One trap: **a whitespace-only text node is assigned to the default slot and
|
|
78
|
+
defeats its fallback.** A newline between the tags is enough. That is
|
|
79
|
+
why some examples close on the same line they open.
|
|
80
|
+
|
|
81
|
+
## Width comes from the layout
|
|
82
|
+
|
|
83
|
+
There is no `width` or `fill` property on a button, for the same reason there is
|
|
84
|
+
no `size` one: how wide a control is says something about the region it is in.
|
|
85
|
+
|
|
86
|
+
A button already stretches in a column flex or a grid, because that is what a
|
|
87
|
+
stretch-aligned parent does to its children:
|
|
88
|
+
|
|
89
|
+
```html
|
|
90
|
+
<helix-flex direction="column">
|
|
91
|
+
<helix-button
|
|
92
|
+
emphasis="solid"
|
|
93
|
+
tone="primary"
|
|
94
|
+
>Fills its column</helix-button>
|
|
95
|
+
</helix-flex>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
To make one hug its label instead, tell the layout:
|
|
99
|
+
|
|
100
|
+
```css
|
|
101
|
+
.hug {
|
|
102
|
+
align-self: start;
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```html
|
|
107
|
+
<helix-button class="hug">Hugs its label</helix-button>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The default surprises people: dropping a button into a column makes it full
|
|
111
|
+
width, and the instinct is to look for a property that turns that off.
|
|
112
|
+
|
|
113
|
+
**A fixed width is a different question, and it belongs to your site.** A
|
|
114
|
+
marketing page whose hero buttons are all the same width is a real and reusable
|
|
115
|
+
practice, but the number is a fact about that page's design, not about buttons.
|
|
116
|
+
Name it once where your other page constants live:
|
|
117
|
+
|
|
118
|
+
```css
|
|
119
|
+
:root {
|
|
120
|
+
--site-cta-inline-size: 13rem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.heroActions helix-button {
|
|
124
|
+
min-inline-size: var(--site-cta-inline-size);
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The same reasoning puts a hero's type size outside the component scale: a
|
|
129
|
+
marketing hero is not a component, so its measurements are the site's own.
|
|
130
|
+
|
|
131
|
+
## One property per intent
|
|
132
|
+
|
|
133
|
+
Every field control names itself the same way, and there are exactly two cases.
|
|
134
|
+
|
|
135
|
+
```html
|
|
136
|
+
<helix-input label="Files"></helix-input> <!-- a visible label -->
|
|
137
|
+
<helix-input aria-label="Files"></helix-input> <!-- named, nothing drawn -->
|
|
138
|
+
<helix-input></helix-input> <!-- unnamed. avoid -->
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`label` means a label. `aria-label` means a name. Neither means a control a
|
|
142
|
+
screen reader announces as "edit text", which is almost never what you want.
|
|
143
|
+
If the page already makes the purpose plain, that is the `aria-label` case, not
|
|
144
|
+
the empty case.
|
|
145
|
+
|
|
146
|
+
**`aria-label` works here because the component forwards it**, which is not true
|
|
147
|
+
of custom elements in general. A custom element
|
|
148
|
+
cannot delegate a label to the control inside its shadow root: an `aria-label`
|
|
149
|
+
you write on the host sits on a wrapper and never reaches the `<input>` that
|
|
150
|
+
actually takes focus. Measured on this library before it was fixed, the inner
|
|
151
|
+
input came back with no label and no name at all.
|
|
152
|
+
|
|
153
|
+
So the field renders the label and clips it, and the name arrives through the
|
|
154
|
+
same `<label for>` a visible one does.
|
|
155
|
+
|
|
156
|
+
There used to be a `hide-label` boolean beside `label`. It is gone: one
|
|
157
|
+
attribute supplying a label and a second saying not to show it is the absence of
|
|
158
|
+
the first, spelled twice.
|
|
159
|
+
|
|
160
|
+
## Shared behavior becomes its own family
|
|
161
|
+
|
|
162
|
+
`Option`, `Listbox` and `Select` are three families rather than one because the
|
|
163
|
+
keyboard, the filtering, the multi-select rules and the active-item tracking
|
|
164
|
+
should be written once. `Select` renders a `Listbox` internally; a future
|
|
165
|
+
combobox will; a command palette would.
|
|
166
|
+
|
|
167
|
+
The same reasoning made `DrilldownController`: `Menu` had held a page stack and
|
|
168
|
+
derived its rows since it was written, and none of that was about menus. `Menu`
|
|
169
|
+
and `Drilldown` share it now.
|
|
170
|
+
|
|
171
|
+
If you are about to write the second copy of something, that is the signal.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Five layers, four shadows, and the glass the material tokens make possible.
|
|
3
|
+
section: Foundations
|
|
4
|
+
title: Elevation and material
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Elevation and material
|
|
8
|
+
|
|
9
|
+
## Layers
|
|
10
|
+
|
|
11
|
+
[//]: # (tokens:layer)
|
|
12
|
+
|
|
13
|
+
| Token | Value |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `--helix-layer-beneath` | `-1` |
|
|
16
|
+
| `--helix-layer-base` | `0` |
|
|
17
|
+
| `--helix-layer-raised` | `10` |
|
|
18
|
+
| `--helix-layer-sticky` | `100` |
|
|
19
|
+
| `--helix-layer-overlay` | `1000` |
|
|
20
|
+
| `--helix-layer-toast` | `1200` |
|
|
21
|
+
|
|
22
|
+
[//]: # (/tokens)
|
|
23
|
+
|
|
24
|
+
Five, named after what lives at each rather than numbered. A numbered scale
|
|
25
|
+
invites `z-index: 1001`, and the number nobody can justify is how stacking
|
|
26
|
+
context bugs start.
|
|
27
|
+
|
|
28
|
+
| Layer | What is at it |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `base` | The document |
|
|
31
|
+
| `raised` | Something lifted but still in the flow: a hovered row, a dragged card |
|
|
32
|
+
| `sticky` | A header or a column that stays while content scrolls under it |
|
|
33
|
+
| `overlay` | Menus, dialogs, popovers, tooltips |
|
|
34
|
+
| `toast` | Above everything, including a modal |
|
|
35
|
+
|
|
36
|
+
**Most overlays do not need these**, because they use the popover API and render
|
|
37
|
+
in the browser's top layer, which is above every z-index by construction. The
|
|
38
|
+
tokens are for the cases the top layer does not cover: your own sticky header,
|
|
39
|
+
your own dragged element, and anything that has to sit between two of ours.
|
|
40
|
+
|
|
41
|
+
## Shadows
|
|
42
|
+
|
|
43
|
+
[//]: # (tokens:shadow)
|
|
44
|
+
|
|
45
|
+
| Token | Value |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `--helix-shadow-none` | `none` |
|
|
48
|
+
| `--helix-shadow-sm` | `0 1px 2px var(--_helix-shadow-ink-sm)` |
|
|
49
|
+
| `--helix-shadow-md` | `0 4px 12px var(--_helix-shadow-ink-md)` |
|
|
50
|
+
| `--helix-shadow-lg` | `0 12px 32px var(--_helix-shadow-ink-lg)` |
|
|
51
|
+
| `--helix-shadow-surface` | `var(--helix-shadow-none)` |
|
|
52
|
+
| `--helix-shadow-floating` | `var(--helix-shadow-lg)` |
|
|
53
|
+
|
|
54
|
+
[//]: # (/tokens)
|
|
55
|
+
|
|
56
|
+
The first four are the values. The last two are the *decision*, and they are
|
|
57
|
+
what a theme sets to make the system shadow-first or border-first. See [Surfaces
|
|
58
|
+
and separation](./surfaces-and-separation).
|
|
59
|
+
|
|
60
|
+
## Material
|
|
61
|
+
|
|
62
|
+
Eight tokens describing translucency rather than color:
|
|
63
|
+
|
|
64
|
+
[//]: # (tokens:material)
|
|
65
|
+
|
|
66
|
+
| Token | Value | For |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| `--helix-material-backdrop` | `blur(var(--helix-material-blur-md)) saturate(1.5)` | The wash behind a modal. |
|
|
69
|
+
| `--helix-material-opacity` | `1` | How opaque a translucent surface is. |
|
|
70
|
+
| `--helix-material-blur-sm` | `8px` | The lightest backdrop blur. |
|
|
71
|
+
| `--helix-material-blur-md` | `16px` | The backdrop blur a modal takes. |
|
|
72
|
+
| `--helix-material-blur-lg` | `32px` | The heaviest backdrop blur. |
|
|
73
|
+
| `--helix-material-translucent` | `color-mix(in oklab, light-dark(oklch(1 0 0), oklch(0.21 var(--helix-seed-neutral-chroma, 0) var(--helix-seed-neutral-hue, 258))) 30%, transparent)` | The translucent ground, for a surface that admits what is behind it. |
|
|
74
|
+
| `--helix-material-translucent-strong` | `color-mix(in oklab, light-dark(oklch(1 0 0), oklch(0.21 var(--helix-seed-neutral-chroma, 0) var(--helix-seed-neutral-hue, 258))) 88%, transparent)` | The heavier translucent ground, for one that admits less. |
|
|
75
|
+
| `--helix-material-border` | `light-dark(color-mix(in oklab, oklch(0.15 var(--helix-seed-neutral-chroma, 0) var(--helix-seed-neutral-hue, 258)) 14%, transparent), color-mix(in oklab, oklch(1 0 0) 12%, transparent))` | The edge a translucent surface needs to have any at all. |
|
|
76
|
+
|
|
77
|
+
[//]: # (/tokens)
|
|
78
|
+
|
|
79
|
+
These are what the `material` axis renders: `frosted`, `glass` and `transparent`
|
|
80
|
+
are three points of one optical language, and they are an axis of their own
|
|
81
|
+
rather than values of `emphasis`, because a panel can be glass *and* outlined
|
|
82
|
+
and two things that can be true at once are two axes. See [Visual
|
|
83
|
+
axes](./visual-axes).
|
|
84
|
+
|
|
85
|
+
**Blur is expensive and is the one thing here worth being deliberate about.** A
|
|
86
|
+
`backdrop-filter` forces the compositor to re-read what is behind the element on
|
|
87
|
+
every frame it moves. A frosted header that is always on screen pays that
|
|
88
|
+
continuously. Reach for `translucent` without the blur when the effect is about
|
|
89
|
+
depth rather than about the texture.
|
|
90
|
+
|
|
91
|
+
**`material-border` is not decoration.** A translucent surface takes its color
|
|
92
|
+
from what is behind it, so on a busy background it has no edge at all. The
|
|
93
|
+
border token is what keeps it a shape.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: The request and the notification, why one name is prefixed and the other is not.
|
|
3
|
+
section: Essentials
|
|
4
|
+
title: Events
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Events
|
|
8
|
+
|
|
9
|
+
**Every state change is announced twice, and the two are for different people.**
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// The request. Cancelable, carries what is being proposed, and fires before
|
|
13
|
+
// anything moves. This is how you take the decision.
|
|
14
|
+
element.addEventListener('helix-before-change', (event) => {
|
|
15
|
+
if (locked) {
|
|
16
|
+
event.preventDefault()
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// The notification, under the platform's own name. Fires after the value moved,
|
|
21
|
+
// so it is on the target where every handler ever written expects it.
|
|
22
|
+
element.addEventListener('change', (event) => {
|
|
23
|
+
save(event.target.value)
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Most consumers want the second. Bind `onChange` in React or `onchange=` in HTML
|
|
28
|
+
and read the target, exactly as you would from an `<input>`. Reach for the
|
|
29
|
+
prefixed one when you mean to refuse.
|
|
30
|
+
|
|
31
|
+
The class exists rather than a bare `CustomEvent` for three reasons, all about
|
|
32
|
+
the type reaching you: the class name is in the manifest, which is what lets the
|
|
33
|
+
framework typings emit a typed callback instead of `(detail: unknown)`; the
|
|
34
|
+
package augments `HTMLElementEventMap`, which types `addEventListener` for
|
|
35
|
+
people writing plain HTML; and the name lives on the class, so a dispatch and a
|
|
36
|
+
listener cannot disagree about the string.
|
|
37
|
+
|
|
38
|
+
## Why the request is prefixed and the notification is not
|
|
39
|
+
|
|
40
|
+
**Retargeting.** An event dispatched inside a shadow root is retargeted to look
|
|
41
|
+
like it came from the host, so a bare `input` from a `<helix-input>` is
|
|
42
|
+
indistinguishable from the inner control's own. Prefixed, it cannot be mistaken
|
|
43
|
+
for either.
|
|
44
|
+
|
|
45
|
+
**React's claimed verbs.** React's synthetic system owns `change`, `input`,
|
|
46
|
+
`submit`, `copy`, `focus` and the rest, and a prop matching one of those is
|
|
47
|
+
wired to the native event instead. `onhelix-before-change` matches none of them
|
|
48
|
+
and reaches us, which is exactly what a request needs.
|
|
49
|
+
|
|
50
|
+
**And the notification wants the opposite.** It is not prefixed precisely
|
|
51
|
+
because React claims the verb: `onChange` on a Helix element goes through the
|
|
52
|
+
synthetic system to the native `change` the element sends, which is why the
|
|
53
|
+
ordinary path costs a consumer nothing to learn.
|
|
54
|
+
|
|
55
|
+
**The type map is global.** `HTMLElementEventMap` is shared with every element
|
|
56
|
+
in your application. Our classes extend `Event`, so an entry named `select`
|
|
57
|
+
would not fail to compile: it would silently retype
|
|
58
|
+
`textarea.addEventListener('select', …)` as carrying our detail, which at
|
|
59
|
+
runtime is `undefined`. Measured. An unprefixed name is not a collision we would
|
|
60
|
+
notice, it is one you would.
|
|
61
|
+
|
|
62
|
+
## Propagation follows the event's role
|
|
63
|
+
|
|
64
|
+
**Lifecycle does not bubble and is not composed.** `helix-before-open` and
|
|
65
|
+
`helix-before-close`. Overlays nest, and an inner one's lifecycle must never reach the
|
|
66
|
+
outer one that contains it: a menu closing inside a dialog is not the dialog
|
|
67
|
+
closing.
|
|
68
|
+
|
|
69
|
+
**Coordination bubbles and is composed.** `helix-select`, `helix-dismiss`,
|
|
70
|
+
`helix-before-change`. A parent, or a router, can intercept one across a shadow
|
|
71
|
+
boundary, which is what makes a `Tree` usable as navigation.
|
|
72
|
+
|
|
73
|
+
If you are listening on a container and hearing nothing, the event is probably a
|
|
74
|
+
lifecycle one and the listener belongs on the element itself.
|
|
75
|
+
|
|
76
|
+
## Canceling
|
|
77
|
+
|
|
78
|
+
The events that precede a state change are cancelable, and that is the only way
|
|
79
|
+
to refuse one:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
dialog.addEventListener('helix-before-close', (event) => {
|
|
83
|
+
if (unsaved) {
|
|
84
|
+
event.preventDefault()
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Every dispatched event is held to a declaration and every declaration to a
|
|
90
|
+
dispatch, so an event you can listen for is an event that is documented.
|
|
91
|
+
|
|
92
|
+
## The set
|
|
93
|
+
|
|
94
|
+
| Event | Roughly |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `helix-before-change` | A value about to move, refusable |
|
|
97
|
+
| `helix-before-toggle` | A checked state about to flip, refusable |
|
|
98
|
+
| `helix-before-open`, `helix-before-close` | An open state about to move, refusable |
|
|
99
|
+
| `helix-select` | An item chosen from a set |
|
|
100
|
+
| `helix-dismiss` | The reader asked for something to go away |
|
|
101
|
+
| `helix-next`, `helix-previous` | Movement through a sequence |
|
|
102
|
+
| `helix-filter` | A query changed in a filtering control |
|
|
103
|
+
| `helix-navigate` | A component is about to navigate |
|
|
104
|
+
| `helix-copy` | Something was copied |
|
|
105
|
+
| `helix-caret` | The cursor moved in a text control |
|
|
106
|
+
| `helix-load`, `helix-error`, `helix-complete` | Media and async lifecycle |
|
|
107
|
+
| `helix-create` | A new item was made from a query |
|
|
108
|
+
| `helix-console` | The sandbox relaying a log |
|
|
109
|
+
| `helix-tour-ask`, `helix-sample-start`, `helix-sample-end` | Tour and color sampling |
|
|
110
|
+
|
|
111
|
+
And the native ones every stateful component sends once the change has landed:
|
|
112
|
+
`input` and `change` on a value control, `toggle` on anything that opens,
|
|
113
|
+
carrying `newState` and `oldState` the way `<details>` does, `close` on a dialog,
|
|
114
|
+
and `load` and `error` on an image.
|
|
115
|
+
|
|
116
|
+
Each component's page lists the events it actually dispatches, with the class
|
|
117
|
+
and the detail.
|
|
118
|
+
|
|
119
|
+
## In a framework
|
|
120
|
+
|
|
121
|
+
A native event is `onChange`, `onInput` or `onToggle`, which is what React and
|
|
122
|
+
every framework already understand. A prefixed one is `on` and the name,
|
|
123
|
+
verbatim: `onhelix-before-change`. Nothing strips the prefix and nothing
|
|
124
|
+
camel-cases it, which is React's own behavior rather than a convention here. See [React](./react), [Vue](./vue) and
|
|
125
|
+
[Svelte](./svelte).
|