@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,399 @@
1
+ ---
2
+ description: What Helix costs a page, measured rather than claimed.
3
+ order: 7
4
+ section: Getting started
5
+ title: Metrics
6
+ ---
7
+
8
+ # Metrics
9
+
10
+ *Generated by `bun run metrics` from the built packages. Do not edit: every
11
+ number here is measured, and an edited one is a number nothing produced.*
12
+
13
+ Bundle size, and what a component costs in time. The two are kept apart on
14
+ purpose: bytes are falsifiable across systems and belong in the comparison,
15
+ and a render timing is a fact about a page, a machine and a day, so it never
16
+ joins one.
17
+
18
+ ## How these are measured
19
+
20
+ The method is on the page because a size table without one is a claim rather
21
+ than a measurement.
22
+
23
+ - **The published artifact, not the source.** Each row bundles `dist`, which is what npm ships, through Bun 1.1.43, minified, ESM, browser target, no source maps.
24
+ - **Everything is bundled in, Lit included.** A page with no other Lit library pays for Lit, so it is counted. Its own row is in the package table below, so it can be subtracted.
25
+ - **A package row re-exports the package; a component row imports it for effect.** That is how each is used: an element is imported to register itself, and a package is measured by its whole public API. Importing a `sideEffects: false` package for effect measures nothing, because a bundler is right to delete it.
26
+ - **Gzip is level 9 and brotli is quality 11**, which is what a CDN serving static assets does.
27
+ - **The bundler version is part of the number.** It is pinned in `packageManager` and in CI, so the same source measures the same in both places.
28
+
29
+ Two columns for every component, because there is no single honest number for
30
+ a component in a library with a shared runtime:
31
+
32
+ - **Alone** is a page that uses this component and nothing else. It includes the runtime the component cannot work without, so it counts that runtime again for every component after the first.
33
+ - **Added** is what this component costs a page that already has the runtime. It excludes what is shared, so it flatters the first component you install.
34
+
35
+ Neither is the real number. The pair brackets it.
36
+
37
+ ## The floor
38
+
39
+ Lit and `@fusion.dev/helix-core`, which is what every element needs and no element
40
+ owns. **Added** is measured against this.
41
+
42
+ **It is larger than most single components below, and that is not a mistake.**
43
+ The floor re-exports the whole of `@fusion.dev/helix-core` so that it is the same size
44
+ under every family. A family that reaches only part of core shakes the rest
45
+ out and comes in smaller than the floor, which is the tree-shaking working:
46
+ nobody ever downloads this floor on its own.
47
+
48
+ | | Raw | Gzip | Brotli |
49
+ |---|---|---|---|
50
+ | Lit and `@fusion.dev/helix-core` | 132,955 | 44,397 | 38,019 |
51
+
52
+ ## Whole packages
53
+
54
+ Everything a package exports, retained. This is the ceiling rather than a
55
+ typical page: `@fusion.dev/helix` and `@fusion.dev/helix-headless` are side-effectful, so
56
+ a barrel import of either really does cost the whole row, and nothing in this
57
+ repository does that. The per-component tables are what a page pays.
58
+
59
+ | Package | Raw | Gzip | Brotli | |
60
+ |---|---|---|---|---|
61
+ | `@fusion.dev/helix-core` | 132,509 | 43,947 | 37,675 | Element base class, mixins, decorators, floating positioning. |
62
+ | `@fusion.dev/helix-headless` | 516,161 | 131,048 | 102,771 | Every headless family, which is what the barrel means. |
63
+ | `@fusion.dev/helix-icons` | 46,295 | 14,855 | 12,542 | The whole glyph set. |
64
+ | `@fusion.dev/helix-theme` | 3,288 | 1,570 | 1,353 | The derivation engine. Build time for most consumers, runtime for a theme switcher. |
65
+ | `@fusion.dev/helix` | 964,531 | 218,272 | 160,502 | Every styled family. |
66
+ | `lit` | 15,394 | 6,118 | 5,460 | Not ours. Here so it can be subtracted from every row above it. |
67
+
68
+ ## The stylesheets
69
+
70
+ Linked rather than bundled, so these are the files exactly as a browser
71
+ receives them. `@fusion.dev/helix-styles` ships its stylesheets with the comments
72
+ removed and keeps the reasoning in the repository, which is why these numbers
73
+ are a quarter of what the sources weigh. It has no JavaScript at all, so this
74
+ is the whole cost of adopting the base layer on its own.
75
+
76
+ | File | Raw | Gzip | Brotli | |
77
+ |---|---|---|---|---|
78
+ | `base.css` | 23,052 | 3,527 | 3,024 | The base layer. Recipes for markup Helix does not render, usable with no JavaScript at all. |
79
+ | `elements.css` | 8,336 | 1,905 | 1,614 | The components' own layer, generated from every family's stylesheet. |
80
+ | `all.css` | 179 | 142 | 110 | The three above in one import, in the order they have to be in. |
81
+ | `primitives.css` | 6,274 | 1,233 | 1,055 | The primitive layer. Private, shipped because `tokens.css` resolves into it. |
82
+ | `tokens.css` | 19,989 | 2,469 | 2,073 | The 185 semantic tokens. Nothing paints without this. |
83
+
84
+ ## What the numbers say
85
+
86
+ **Most of a first component is the runtime under it.** The median styled family is 13,976 bytes gzipped alone and adds 2,475 to a page that already has Helix on it, so 82 percent of the first one is shared and 18 percent is the component. Ten typical components are one plus nine additions rather than ten of the first number, and the pair of columns below is what lets that be worked out rather than guessed.
87
+
88
+ Across the tier: the cheapest is `code` at 9,057 gzipped alone, the dearest `data-table` at 49,471, over 93 components. Every family at once is 218,272.
89
+
90
+ **No family imports a barrel, and `check:barrels` is what keeps it that way.** `@fusion.dev/helix-headless` is `sideEffects` over every module in `dist`, which is true of a file that calls `customElements.define` and which means a bundler may drop none of it: one `import { HelixHeadlessButton } from '@fusion.dev/helix-headless'` costs 131,048 bytes gzipped whatever else the page does, which is 9 times the median family measured above. The styled tier imports the family it extends instead, all 93 of them, and the check refuses the shape rather than the package name, so the rule follows `sideEffects` wherever it goes.
91
+
92
+ The headless tier never had the problem, and it is the control: `@fusion.dev/helix-core` is `sideEffects: false`, and a headless family is a median of 16,084 bytes gzipped alone.
93
+
94
+ ## Against other systems
95
+
96
+ Four other libraries of custom elements, pinned and measured on 2026-08-24 by `bun run metrics:peers`, which installs them into `.local` and bundles them through the harness that produced everything above. **Helix's own rows are measured on every run of `bun run metrics`**, so the column that could go stale against this repository is the one that cannot.
97
+
98
+ **Every system here is built on Lit**, so the runtime under the comparison is the same library in every column and what is left is the components. A React component library is not in the table, because comparing a custom element with a React component measures the framework.
99
+
100
+ **There is no whole-library row**, deliberately. It would compare how many components each vendor decided to ship, and how each one counts a component, which is the number that looks most like a measurement and means least. Five components every one of these has is the comparison.
101
+
102
+ | Component | System | Raw | Gzip | Brotli |
103
+ |---|---|---|---|---|
104
+ | `button` | **Helix** | 37,328 | 11,515 | 10,158 |
105
+ | | Carbon | 133,058 | 20,959 | 17,685 |
106
+ | | Material Web | 47,060 | 14,173 | 12,591 |
107
+ | | Shoelace | 62,628 | 18,287 | 15,805 |
108
+ | | Spectrum | 98,792 | 22,045 | 18,674 |
109
+ | `checkbox` | **Helix** | 40,872 | 12,578 | 11,068 |
110
+ | | Carbon | 60,255 | 15,609 | 13,550 |
111
+ | | Material Web | 43,785 | 13,556 | 12,015 |
112
+ | | Shoelace | 47,614 | 15,177 | 13,301 |
113
+ | | Spectrum | 62,492 | 15,437 | 13,481 |
114
+ | `dialog` | **Helix** | 34,636 | 11,191 | 9,886 |
115
+ | | Carbon | 510,866 | 67,390 | 39,189 |
116
+ | | Material Web | 34,238 | 11,510 | 10,229 |
117
+ | | Shoelace | 52,754 | 17,698 | 15,554 |
118
+ | | Spectrum | 155,708 | 31,899 | 26,262 |
119
+ | `radio` | **Helix** | 43,716 | 12,868 | 11,346 |
120
+ | | Carbon | 63,055 | 16,887 | 14,605 |
121
+ | | Material Web | 41,044 | 13,234 | 11,757 |
122
+ | | Shoelace | 34,865 | 12,027 | 10,559 |
123
+ | | Spectrum | 38,685 | 10,660 | 9,370 |
124
+ | `switch` | **Helix** | 38,547 | 12,144 | 10,699 |
125
+ | | Carbon | 69,423 | 17,127 | 14,764 |
126
+ | | Material Web | 50,155 | 14,577 | 12,935 |
127
+ | | Shoelace | 37,833 | 11,814 | 10,568 |
128
+ | | Spectrum | 62,322 | 15,059 | 13,321 |
129
+ | `tabs` | **Helix** | 40,442 | 12,374 | 10,893 |
130
+ | | Carbon | 578,766 | 73,537 | 43,235 |
131
+ | | Material Web | 38,883 | 12,820 | 11,368 |
132
+ | | Shoelace | 54,573 | 17,140 | 15,068 |
133
+ | | Spectrum | 64,875 | 18,298 | 16,283 |
134
+ | `tooltip` | **Helix** | 59,436 | 20,247 | 17,886 |
135
+ | | Carbon | 189,382 | 30,354 | 22,273 |
136
+ | | Material Web | | not shipped | |
137
+ | | Shoelace | 57,412 | 20,220 | 17,933 |
138
+ | | Spectrum | 136,599 | 40,552 | 35,286 |
139
+
140
+ **Where Helix loses**, which is the half of a comparison table worth reading: `radio`, `switch`, `tooltip`. The largest gap is radio against Spectrum, by 2,208 bytes gzipped. 4 of 28 peer measurements come in under ours.
141
+
142
+ - **Carbon** `@carbon/web-components@2.61.0`. Its dialog is Modal, its radio is Radio Button, and its switch is Toggle.
143
+ - **Material Web** `@material/web@2.5.0`. Ships no tooltip. Its button is measured as the filled one, there being no single button element.
144
+ - **Shoelace** `@shoelace-style/shoelace@2.20.1`. Its tabs element is Tab Group.
145
+ - **Spectrum** `@spectrum-web-components/button@1.12.2`. One package per component, so there is no barrel to import by accident.
146
+
147
+ Every specifier measured is in [`peers.json`](https://github.com/project-fusion/fusion/blob/main/helix/docs/peers.json), so a disagreement with these numbers is a thing that can be run rather than argued.
148
+
149
+ ## What it costs in time
150
+
151
+ Measured on 2026-08-26 by `bun run metrics:timing`, which drives a real browser against the built packages. The median of 9 runs each, on one machine.
152
+
153
+ **These never join the comparison above, and that is not a hedge.** Bytes are a fact about a package and can be held against anybody else's. A timing is a fact about a page, a machine and a day, so a column of somebody else's milliseconds would be comparing two laptops rather than two libraries.
154
+
155
+ **Nothing checks these.** A timing moves several percent between runs on one machine and much further between machines, so a gate on it would fail for reasons nobody caused, which is the fastest way to teach a team that red means nothing. `verify` does not run it. It is refreshed by hand, the same as the peer table, and the date above is the answer to how current it is.
156
+
157
+ | Component | Time to upgrade | First render | Hydration |
158
+ | --- | --- | --- | --- |
159
+ | `button` | 5.80ms | 0.10ms | 6.30ms |
160
+ | `checkbox` | 7.00ms | 0.10ms | 6.50ms |
161
+ | `dialog` | 6.20ms | 0.10ms | 6.60ms |
162
+ | `tabs` | 6.60ms | 0.10ms | 6.90ms |
163
+ | `tooltip` | 6.90ms | 0.10ms | 7.00ms |
164
+
165
+ **Time to upgrade** is the gap between markup being in the document and its shadow root existing, which is the flash of unstyled content that server rendering exists to remove. It is measured the way it happens: the markup is parsed first and the definition arrives after, which is what a script at the foot of a page does. It is dominated by fetching and evaluating the module, so it tracks the byte tables above rather than saying something separate from them.
166
+
167
+ **First render** is what one fresh element costs once the definition is already there, from construction to its first update completing. It is mostly Lit's and is close to identical across families, which is the point: a family far off it is doing something in its first update that the others are not.
168
+
169
+ **Hydration** is the gap between a server-rendered tree sitting in the document and that same tree being live. It is the number server rendering exists to justify and the easiest of the three to fake, so it is measured against a document the server actually rendered and delivered: assigning that markup with `innerHTML` does not parse a declarative shadow root, so there would be nothing to adopt and every reading would be a rebuild wearing the right label.
170
+
171
+ **Every figure above was checked for adoption rather than assumed to have it**, and the check is that adoption adds nothing: the count of parts in the shadow root before the definition arrives has to equal the count after. Marking a node and looking for it later passes either way, because Lit does not clear a shadow root it did not create, so a failed hydration leaves the mark in place and appends a second copy of the tree beneath it. Measured on Button with the support module removed, the parts went from one to two while the timing moved by half a millisecond, so without the adoption test the wrong number is indistinguishable from the right one. A row reading `(rebuilt)` is that failure, and there are none.
172
+
173
+ ## Per component
174
+
175
+ Alphabetical, so this is a lookup table. Bytes, not kilobytes, because a
176
+ rounded number in a generated table is a number somebody has to un-round
177
+ before they can check it.
178
+
179
+ ### Headless tier
180
+
181
+ Structure, accessibility, state and form participation. No presentation, and no stylesheet to load beside it.
182
+
183
+ | Family | Alone raw | Alone gzip | Alone brotli | Added raw | Added gzip | Added brotli |
184
+ |---|---|---|---|---|---|---|
185
+ | `accordion` | 76,223 | 25,761 | 21,340 | 3,227 | 1,222 | 655 |
186
+ | `announcement` | 74,254 | 25,126 | 20,904 | 1,374 | 684 | 205 |
187
+ | `app-header` | 27,483 | 9,331 | 8,226 | 1,306 | 611 | 203 |
188
+ | `avatar` | 28,859 | 9,794 | 8,643 | 2,644 | 1,070 | 582 |
189
+ | `badge` | 26,919 | 9,246 | 8,148 | 1,120 | 570 | 195 |
190
+ | `bar-chart` | 70,700 | 24,232 | 21,400 | 12,138 | 4,122 | 2,902 |
191
+ | `blockquote` | 26,839 | 9,232 | 8,140 | 917 | 523 | 98 |
192
+ | `breadcrumbs` | 73,875 | 24,790 | 20,711 | 1,955 | 727 | 354 |
193
+ | `button` | 31,158 | 10,389 | 9,175 | 3,167 | 1,125 | 613 |
194
+ | `button-group` | 26,832 | 9,170 | 8,068 | 661 | 406 | 108 |
195
+ | `callout` | 73,005 | 24,653 | 20,548 | 912 | 524 | 175 |
196
+ | `card` | 26,866 | 9,186 | 8,102 | 694 | 420 | 51 |
197
+ | `carousel` | 79,754 | 26,720 | 22,285 | 7,686 | 2,448 | 1,616 |
198
+ | `chat` | 83,713 | 27,957 | 23,221 | 9,449 | 2,829 | 1,918 |
199
+ | `checkbox` | 34,492 | 11,126 | 9,843 | 2,252 | 1,011 | 371 |
200
+ | `checkbox-group` | 37,392 | 11,749 | 10,393 | 4,655 | 1,633 | 878 |
201
+ | `code` | 25,451 | 8,754 | 7,715 | 451 | 292 | 9 |
202
+ | `code-block` | 111,326 | 37,528 | 31,778 | 12,439 | 3,555 | 2,534 |
203
+ | `color-input` | 129,288 | 42,299 | 35,792 | 26,102 | 7,166 | 5,692 |
204
+ | `color-picker` | 99,094 | 32,311 | 26,815 | 19,492 | 5,690 | 4,497 |
205
+ | `combobox` | 125,346 | 40,786 | 34,413 | 20,361 | 5,532 | 4,171 |
206
+ | `command` | 99,183 | 31,859 | 26,261 | 17,134 | 4,872 | 3,598 |
207
+ | `copy-button` | 110,006 | 37,256 | 31,540 | 11,120 | 3,302 | 2,246 |
208
+ | `data-table` | 141,668 | 45,322 | 38,167 | 37,185 | 9,848 | 7,800 |
209
+ | `date-input` | 122,127 | 40,161 | 33,994 | 19,388 | 5,390 | 4,091 |
210
+ | `date-picker` | 84,476 | 28,217 | 23,393 | 9,805 | 3,185 | 2,227 |
211
+ | `dialog` | 30,301 | 10,183 | 8,978 | 2,534 | 1,081 | 560 |
212
+ | `divider` | 27,036 | 9,308 | 8,172 | 1,115 | 596 | 170 |
213
+ | `donut-chart` | 66,610 | 22,813 | 20,207 | 9,398 | 3,290 | 2,302 |
214
+ | `drawer` | 29,268 | 9,876 | 8,699 | 1,792 | 798 | 355 |
215
+ | `drilldown` | 79,507 | 26,788 | 22,064 | 5,059 | 1,765 | 1,078 |
216
+ | `empty` | 26,988 | 9,226 | 8,147 | 816 | 473 | 74 |
217
+ | `file-input` | 84,086 | 28,154 | 23,166 | 6,219 | 2,205 | 1,428 |
218
+ | `format-date` | 30,257 | 10,284 | 9,103 | 2,805 | 972 | 520 |
219
+ | `heatmap` | 68,075 | 23,096 | 20,397 | 10,692 | 3,537 | 2,391 |
220
+ | `icon` | 72,411 | 24,410 | 20,339 | 850 | 487 | 75 |
221
+ | `icon-button` | 31,557 | 10,419 | 9,198 | 3,566 | 1,153 | 661 |
222
+ | `image` | 28,816 | 9,694 | 8,539 | 2,969 | 1,059 | 606 |
223
+ | `input` | 83,600 | 27,860 | 22,929 | 4,944 | 1,773 | 1,074 |
224
+ | `kbd` | 28,236 | 9,743 | 8,582 | 2,427 | 1,131 | 568 |
225
+ | `legend` | 29,187 | 10,066 | 8,845 | 1,097 | 600 | 176 |
226
+ | `line-chart` | 68,726 | 23,631 | 20,885 | 10,165 | 3,525 | 2,540 |
227
+ | `list` | 27,887 | 9,356 | 8,240 | 1,727 | 588 | 278 |
228
+ | `listbox` | 91,239 | 30,089 | 24,864 | 10,677 | 3,397 | 2,343 |
229
+ | `loader` | 27,072 | 9,293 | 8,182 | 900 | 503 | 64 |
230
+ | `megamenu` | 55,180 | 19,332 | 17,079 | 4,934 | 1,741 | 1,043 |
231
+ | `menu` | 108,430 | 36,763 | 31,227 | 8,105 | 2,390 | 1,596 |
232
+ | `meter` | 29,964 | 10,078 | 8,894 | 3,800 | 1,271 | 786 |
233
+ | `navigation` | 28,831 | 9,815 | 8,680 | 1,582 | 720 | 218 |
234
+ | `notifications` | 83,706 | 28,017 | 23,156 | 8,640 | 2,592 | 1,723 |
235
+ | `number-input` | 85,670 | 28,393 | 23,470 | 7,100 | 2,322 | 1,489 |
236
+ | `option` | 28,849 | 9,632 | 8,502 | 2,680 | 896 | 466 |
237
+ | `outline` | 31,837 | 10,814 | 9,540 | 4,539 | 1,772 | 1,039 |
238
+ | `pagination` | 130,381 | 41,800 | 35,277 | 26,097 | 6,594 | 5,115 |
239
+ | `pin-input` | 37,381 | 11,959 | 10,576 | 5,135 | 1,862 | 1,159 |
240
+ | `popover` | 52,511 | 18,611 | 16,413 | 2,019 | 873 | 397 |
241
+ | `progress` | 28,113 | 9,563 | 8,430 | 1,949 | 788 | 421 |
242
+ | `qr-code` | 38,587 | 13,556 | 11,956 | 12,807 | 4,961 | 4,012 |
243
+ | `quote` | 26,355 | 9,041 | 7,964 | 552 | 402 | 103 |
244
+ | `radio` | 35,977 | 11,448 | 10,083 | 3,138 | 1,162 | 529 |
245
+ | `rating` | 38,033 | 12,237 | 10,822 | 5,617 | 2,031 | 1,258 |
246
+ | `scroll-area` | 28,463 | 9,741 | 8,560 | 2,610 | 1,108 | 492 |
247
+ | `select` | 119,544 | 39,510 | 33,314 | 15,389 | 4,432 | 3,180 |
248
+ | `skeleton` | 25,544 | 8,786 | 7,725 | 544 | 354 | 6 |
249
+ | `slider` | 37,742 | 12,019 | 10,633 | 5,250 | 1,817 | 1,153 |
250
+ | `sparkline` | 31,645 | 11,086 | 9,794 | 2,530 | 1,085 | 618 |
251
+ | `speed-dial` | 100,991 | 34,766 | 29,474 | 3,821 | 1,299 | 714 |
252
+ | `splitter` | 36,329 | 11,666 | 10,300 | 10,165 | 2,926 | 2,149 |
253
+ | `stat` | 27,320 | 9,222 | 8,136 | 1,150 | 459 | 85 |
254
+ | `steps` | 81,596 | 27,126 | 22,419 | 6,303 | 1,864 | 1,145 |
255
+ | `switch` | 34,160 | 11,058 | 9,766 | 1,798 | 860 | 336 |
256
+ | `table` | 31,609 | 10,196 | 9,008 | 4,118 | 1,226 | 758 |
257
+ | `tabs` | 33,935 | 11,037 | 9,724 | 4,469 | 1,303 | 719 |
258
+ | `tag` | 73,289 | 24,733 | 20,562 | 897 | 492 | 128 |
259
+ | `textarea` | 36,847 | 11,696 | 10,315 | 4,115 | 1,416 | 757 |
260
+ | `time-input` | 134,184 | 43,285 | 36,521 | 28,643 | 7,663 | 6,078 |
261
+ | `time-picker` | 99,260 | 32,521 | 26,869 | 18,154 | 5,510 | 4,100 |
262
+ | `timeline` | 73,821 | 24,751 | 20,746 | 1,896 | 686 | 324 |
263
+ | `toast` | 77,019 | 25,851 | 21,418 | 4,007 | 1,278 | 790 |
264
+ | `toggle` | 38,209 | 12,108 | 10,665 | 3,928 | 1,330 | 703 |
265
+ | `toolbar` | 28,734 | 9,827 | 8,655 | 2,932 | 1,258 | 610 |
266
+ | `tooltip` | 55,871 | 19,441 | 17,199 | 5,867 | 2,058 | 1,213 |
267
+ | `tour` | 61,086 | 20,604 | 18,240 | 10,277 | 2,947 | 2,027 |
268
+ | `tree` | 83,288 | 27,803 | 22,913 | 7,069 | 2,379 | 1,471 |
269
+ | `trend` | 72,614 | 24,511 | 20,430 | 1,056 | 561 | 140 |
270
+ | `video` | 131,880 | 42,504 | 36,213 | 31,304 | 7,765 | 6,491 |
271
+ | `video-playlist` | 115,879 | 38,435 | 32,636 | 14,151 | 3,705 | 2,679 |
272
+ | `virtual-list` | 29,714 | 10,244 | 9,021 | 2,089 | 964 | 480 |
273
+
274
+ ### Styled tier
275
+
276
+ The headless tier plus presentation. This is what `bun add @fusion.dev/helix` gives you.
277
+
278
+ | Family | Alone raw | Alone gzip | Alone brotli | Added raw | Added gzip | Added brotli |
279
+ |---|---|---|---|---|---|---|
280
+ | `accordion` | 79,518 | 26,569 | 21,911 | 6,510 | 2,049 | 1,327 |
281
+ | `announcement` | 78,022 | 26,038 | 21,534 | 5,126 | 1,535 | 995 |
282
+ | `app-header` | 30,814 | 10,122 | 8,910 | 4,620 | 1,375 | 845 |
283
+ | `aura` | 93,124 | 32,615 | 28,199 | 66,161 | 23,230 | 20,023 |
284
+ | `avatar` | 32,433 | 10,538 | 9,282 | 6,205 | 1,798 | 1,187 |
285
+ | `badge` | 29,550 | 9,942 | 8,737 | 3,734 | 1,211 | 784 |
286
+ | `bar-chart` | 86,453 | 26,999 | 23,818 | 27,871 | 6,944 | 5,289 |
287
+ | `blockquote` | 28,015 | 9,529 | 8,383 | 2,077 | 793 | 367 |
288
+ | `breadcrumbs` | 77,346 | 25,597 | 21,315 | 5,414 | 1,491 | 1,010 |
289
+ | `button` | 37,328 | 11,515 | 10,158 | 9,319 | 2,274 | 1,635 |
290
+ | `button-group` | 28,508 | 9,543 | 8,424 | 2,320 | 783 | 404 |
291
+ | `callout` | 75,023 | 25,222 | 20,989 | 2,914 | 1,037 | 632 |
292
+ | `card` | 30,062 | 9,999 | 8,796 | 3,878 | 1,197 | 857 |
293
+ | `carousel` | 88,795 | 28,265 | 23,685 | 16,716 | 3,981 | 2,976 |
294
+ | `chat` | 95,392 | 30,099 | 25,048 | 21,117 | 4,948 | 3,809 |
295
+ | `checkbox` | 40,872 | 12,578 | 11,068 | 8,616 | 2,460 | 1,714 |
296
+ | `checkbox-group` | 39,639 | 12,321 | 10,884 | 6,888 | 2,215 | 1,375 |
297
+ | `code` | 26,476 | 9,057 | 7,994 | 1,459 | 598 | 175 |
298
+ | `code-block` | 124,510 | 39,736 | 33,611 | 25,604 | 5,672 | 4,385 |
299
+ | `color-input` | 161,123 | 47,164 | 40,047 | 57,917 | 11,996 | 10,000 |
300
+ | `color-picker` | 125,641 | 36,555 | 30,553 | 46,033 | 9,895 | 8,289 |
301
+ | `combobox` | 139,026 | 43,268 | 36,586 | 34,020 | 7,937 | 6,234 |
302
+ | `command` | 109,436 | 33,866 | 28,080 | 27,372 | 6,828 | 5,298 |
303
+ | `copy-button` | 120,632 | 39,134 | 33,084 | 21,728 | 5,089 | 3,840 |
304
+ | `data-table` | 169,562 | 49,471 | 41,812 | 65,060 | 14,048 | 11,315 |
305
+ | `date-input` | 137,056 | 43,024 | 36,502 | 34,295 | 8,187 | 6,693 |
306
+ | `date-picker` | 90,171 | 29,433 | 24,478 | 15,482 | 4,357 | 3,290 |
307
+ | `dialog` | 34,636 | 11,191 | 9,886 | 6,854 | 2,117 | 1,412 |
308
+ | `divider` | 29,181 | 9,821 | 8,631 | 3,243 | 1,074 | 656 |
309
+ | `donut-chart` | 78,627 | 24,829 | 21,927 | 21,398 | 5,370 | 3,962 |
310
+ | `drawer` | 33,215 | 10,783 | 9,504 | 5,721 | 1,692 | 1,135 |
311
+ | `drilldown` | 83,865 | 27,759 | 22,977 | 9,404 | 2,737 | 1,944 |
312
+ | `empty` | 28,295 | 9,601 | 8,463 | 2,107 | 826 | 412 |
313
+ | `file-input` | 94,830 | 30,332 | 25,049 | 16,956 | 4,366 | 3,243 |
314
+ | `flex` | 27,799 | 9,391 | 8,289 | 1,986 | 698 | 351 |
315
+ | `format-date` | 30,905 | 10,420 | 9,233 | 3,435 | 1,135 | 531 |
316
+ | `grid` | 28,326 | 9,563 | 8,437 | 2,511 | 871 | 525 |
317
+ | `heatmap` | 71,868 | 23,944 | 21,216 | 14,467 | 4,404 | 3,164 |
318
+ | `icon` | 73,106 | 24,587 | 20,429 | 1,528 | 627 | 270 |
319
+ | `icon-button` | 37,656 | 11,455 | 10,088 | 9,651 | 2,208 | 1,604 |
320
+ | `image` | 30,646 | 10,202 | 8,977 | 4,781 | 1,555 | 1,038 |
321
+ | `input` | 89,818 | 29,243 | 24,183 | 11,157 | 3,138 | 2,169 |
322
+ | `kbd` | 29,648 | 10,205 | 8,982 | 3,824 | 1,584 | 1,038 |
323
+ | `legend` | 34,022 | 11,144 | 9,798 | 5,924 | 1,707 | 1,104 |
324
+ | `line-chart` | 81,226 | 25,771 | 22,754 | 22,646 | 5,721 | 4,207 |
325
+ | `list` | 31,940 | 10,289 | 9,061 | 5,770 | 1,526 | 1,095 |
326
+ | `listbox` | 95,298 | 31,052 | 25,548 | 14,723 | 4,350 | 3,093 |
327
+ | `loader` | 30,078 | 10,033 | 8,831 | 3,891 | 1,192 | 862 |
328
+ | `megamenu` | 59,602 | 20,377 | 18,025 | 9,341 | 2,802 | 2,020 |
329
+ | `menu` | 114,709 | 38,183 | 32,425 | 14,366 | 3,748 | 2,813 |
330
+ | `meter` | 35,602 | 11,267 | 9,972 | 9,420 | 2,475 | 1,903 |
331
+ | `navigation` | 30,359 | 10,243 | 9,007 | 3,094 | 1,127 | 575 |
332
+ | `notifications` | 91,354 | 29,582 | 24,616 | 16,275 | 4,146 | 3,062 |
333
+ | `number-input` | 90,634 | 29,544 | 24,415 | 12,050 | 3,439 | 2,486 |
334
+ | `option` | 32,116 | 10,542 | 9,321 | 5,935 | 1,785 | 1,141 |
335
+ | `outline` | 35,824 | 11,618 | 10,255 | 8,509 | 2,589 | 1,722 |
336
+ | `pagination` | 148,273 | 44,851 | 37,991 | 43,971 | 9,539 | 7,685 |
337
+ | `pin-input` | 40,603 | 12,787 | 11,279 | 8,340 | 2,707 | 1,832 |
338
+ | `popover` | 55,129 | 19,245 | 17,022 | 4,619 | 1,500 | 983 |
339
+ | `progress` | 33,246 | 10,757 | 9,521 | 7,064 | 1,972 | 1,448 |
340
+ | `qr-code` | 40,238 | 13,976 | 12,338 | 14,441 | 5,391 | 4,349 |
341
+ | `quote` | 27,030 | 9,207 | 8,088 | 1,211 | 570 | 159 |
342
+ | `radio` | 43,716 | 12,868 | 11,346 | 10,869 | 2,616 | 1,892 |
343
+ | `rating` | 41,008 | 13,168 | 11,593 | 8,574 | 2,951 | 2,075 |
344
+ | `sandbox` | 61,201 | 16,813 | 14,897 | 34,739 | 8,029 | 6,423 |
345
+ | `scroll-area` | 31,437 | 10,374 | 9,095 | 5,567 | 1,762 | 1,019 |
346
+ | `select` | 131,872 | 41,805 | 35,381 | 27,696 | 6,674 | 5,111 |
347
+ | `skeleton` | 28,062 | 9,544 | 8,416 | 2,242 | 860 | 426 |
348
+ | `slider` | 47,682 | 13,817 | 12,221 | 15,173 | 3,610 | 2,730 |
349
+ | `sparkline` | 35,412 | 12,013 | 10,609 | 6,285 | 2,018 | 1,464 |
350
+ | `speed-dial` | 108,524 | 36,260 | 30,765 | 11,332 | 2,750 | 2,014 |
351
+ | `splitter` | 42,741 | 12,722 | 11,288 | 16,559 | 3,995 | 3,131 |
352
+ | `stat` | 29,954 | 9,789 | 8,646 | 3,774 | 1,039 | 614 |
353
+ | `steps` | 95,132 | 29,536 | 24,464 | 19,827 | 4,267 | 3,406 |
354
+ | `switch` | 38,547 | 12,144 | 10,699 | 6,170 | 1,934 | 1,308 |
355
+ | `table` | 34,938 | 10,790 | 9,513 | 7,436 | 1,842 | 1,199 |
356
+ | `tabs` | 40,442 | 12,374 | 10,893 | 10,965 | 2,669 | 1,847 |
357
+ | `tag` | 76,483 | 25,615 | 21,389 | 4,076 | 1,285 | 897 |
358
+ | `text` | 29,734 | 9,670 | 8,508 | 3,922 | 994 | 512 |
359
+ | `textarea` | 40,278 | 12,528 | 11,028 | 7,529 | 2,288 | 1,518 |
360
+ | `time-input` | 154,142 | 46,671 | 39,499 | 48,582 | 10,985 | 8,883 |
361
+ | `time-picker` | 110,619 | 34,662 | 28,765 | 29,508 | 7,635 | 5,964 |
362
+ | `timeline` | 77,087 | 25,507 | 21,266 | 5,151 | 1,394 | 978 |
363
+ | `toast` | 84,299 | 27,460 | 22,823 | 11,274 | 2,899 | 2,145 |
364
+ | `toggle` | 43,388 | 13,293 | 11,734 | 9,099 | 2,562 | 1,840 |
365
+ | `toolbar` | 29,632 | 10,054 | 8,841 | 3,813 | 1,495 | 838 |
366
+ | `tooltip` | 59,436 | 20,247 | 17,886 | 9,414 | 2,863 | 1,933 |
367
+ | `tour` | 71,124 | 22,307 | 19,738 | 20,296 | 4,649 | 3,493 |
368
+ | `tree` | 88,512 | 29,043 | 24,005 | 12,281 | 3,611 | 2,628 |
369
+ | `trend` | 75,648 | 25,206 | 21,009 | 4,083 | 1,215 | 769 |
370
+ | `video` | 149,490 | 45,696 | 38,857 | 48,893 | 10,889 | 9,107 |
371
+ | `video-playlist` | 128,650 | 40,737 | 34,547 | 26,903 | 5,922 | 4,697 |
372
+ | `virtual-list` | 30,546 | 10,470 | 9,192 | 2,904 | 1,181 | 649 |
373
+
374
+ ## What lands in `node_modules`
375
+
376
+ A different question from what reaches a browser, and the one an install
377
+ budget is written against. Unpacked bytes of exactly what each package
378
+ publishes, which is its `files` field. **Source maps are not in these
379
+ numbers**: they are stripped from the tarball at pack time, so the last
380
+ column is what was removed rather than part of what remains.
381
+
382
+ | Package | Unpacked | Files | Source maps stripped | Runtime dependencies |
383
+ |---|---|---|---|---|
384
+ | `@fusion.dev/helix-core` | 910,682 | 113 | 160,137 | `@floating-ui/dom`, `@fusion.dev/helix-icons` |
385
+ | `@fusion.dev/helix-headless` | 10,110,995 | 422 | 737,978 | `@fusion.dev/helix-core`, `@fusion.dev/helix-icons`, `uqr` |
386
+ | `@fusion.dev/helix-icons` | 65,119 | 11 | 20,405 | none |
387
+ | `@fusion.dev/helix-styles` | 83,183 | 5 | 0 | none |
388
+ | `@fusion.dev/helix-theme` | 465,772 | 8 | 4,793 | none |
389
+ | `@fusion.dev/helix` | 10,029,875 | 717 | 287,558 | `@fusion.dev/helix-core`, `@fusion.dev/helix-headless`, `@fusion.dev/helix-styles` |
390
+
391
+ ## Not measured yet
392
+
393
+ Named rather than left out, because a metrics page is read for what it does
394
+ not say as much as for what it does.
395
+
396
+ - **No whole-library comparison, and no fifth component.** The comparison above is five components against four systems. Widening it means naming the equivalent element in each system by hand, which is where a comparison table starts arguing, so each addition is a deliberate one rather than a sweep.
397
+ - **The peer numbers are pinned, so they age.** They are dated in the section above and refreshed by running `bun run metrics:peers`. Nothing fails when they are old, because nothing in this repository changes when somebody else releases, and a check that fired on the calendar would be noise.
398
+ - **No load or rendering timings.** First paint, time to upgrade, and the cost of hydrating a server-rendered tree. These need a browser harness rather than a bundler, and they never join a cross-system table.
399
+ - **No per-component stylesheet bytes.** There are none to report: a family's CSS is compiled into its JavaScript, so it is already inside every number above. `elements.css` is the light-DOM layer and is in the stylesheet table.
@@ -0,0 +1,91 @@
1
+ ---
2
+ description: Four durations, five curves, and the rule every animating component has to answer.
3
+ section: Foundations
4
+ title: Motion
5
+ ---
6
+
7
+ # Motion
8
+
9
+ ## The tokens
10
+
11
+ [//]: # (tokens:duration)
12
+
13
+ | Token | Value |
14
+ | --- | --- |
15
+ | `--helix-duration-instant` | `0ms` |
16
+ | `--helix-duration-fast` | `120ms` |
17
+ | `--helix-duration-normal` | `200ms` |
18
+ | `--helix-duration-slow` | `320ms` |
19
+
20
+ [//]: # (/tokens)
21
+
22
+ [//]: # (tokens:easing)
23
+
24
+ | Token | Value |
25
+ | --- | --- |
26
+ | `--helix-easing-standard` | `cubic-bezier(0.2, 0, 0, 1)` |
27
+ | `--helix-easing-emphasized` | `cubic-bezier(0.3, 0, 0, 1.2)` |
28
+ | `--helix-easing-decelerate` | `cubic-bezier(0, 0, 0, 1)` |
29
+ | `--helix-easing-accelerate` | `cubic-bezier(0.3, 0, 1, 1)` |
30
+ | `--helix-easing-spring` | `linear(0, 0.006, 0.025, 0.101, 0.539, 0.826, 0.976, 1.037, 1.043, 1.026, 1.006, 0.997, 1)` |
31
+
32
+ [//]: # (/tokens)
33
+
34
+ Four durations and five curves. `standard` for most things, `decelerate` for
35
+ something arriving, `accelerate` for something leaving, `emphasized` for a
36
+ movement the reader is meant to follow, and `spring` for the few places
37
+ overshoot reads as responsive rather than as a bug.
38
+
39
+ The theme engine takes a `motion` intent input, so a product can be uniformly
40
+ quicker or slower without every component being retuned.
41
+
42
+ ## Reduced motion is per component
43
+
44
+ The base layer has a global rule capping animation and transition duration for a
45
+ reader who asked not to be moved. **It does not reach a single component.**
46
+
47
+ ```css
48
+ *, *::before, *::after { … }
49
+ ```
50
+
51
+ `*` does not cross a shadow boundary. So that rule reaches your prose, your
52
+ layout and the base layer's own recipes, and none of the ninety-odd components
53
+ that draw inside a shadow root. Each one answers `prefers-reduced-motion` in its
54
+ own stylesheet, fifty-nine of them do because fifty-nine of them animate, and a
55
+ component that animates without one fails the build.
56
+
57
+ **The same is true of yours.** If you write a component with a shadow root, the
58
+ global rule will not cover it either. That is not a Helix behavior, it is how
59
+ `*` works, and it is the single easiest accessibility regression to ship without
60
+ noticing.
61
+
62
+ ```css
63
+ @media (prefers-reduced-motion: reduce) {
64
+ :host {
65
+ animation: none;
66
+ transition: none;
67
+ }
68
+ }
69
+ ```
70
+
71
+ ## What reduced motion means here
72
+
73
+ Not "no movement". A transition that communicates a relationship, like a panel
74
+ sliding out of the control that opened it, is doing work; a decorative float is
75
+ not. The library's own policy is to remove the second and shorten the first to
76
+ the point where it is instant but not jarring.
77
+
78
+ The one place this is absolute is anything that moves continuously without
79
+ input: the aura shader, marquees, and looping loaders all stop rather than speed
80
+ up.
81
+
82
+ ## Two things that are not motion tokens
83
+
84
+ **`@starting-style`** is how overlays animate in, and it needs no duration token
85
+ of its own: it is a starting value, and the transition that runs from it uses
86
+ the tokens above.
87
+
88
+ **View transitions** are not used anywhere in the library. A component cannot
89
+ own a view transition, because the transition is between two states of the
90
+ document rather than of the component, so if you want them that is yours to
91
+ drive.
@@ -0,0 +1,70 @@
1
+ ---
2
+ description: The three rules every framework reduces to, and what they look like in each.
3
+ parent: installation
4
+ section: Getting started
5
+ title: Other frameworks
6
+ trailing: true
7
+ ---
8
+
9
+ # Other frameworks
10
+
11
+ Solid, Qwik, Lit, Ember, Alpine, htmx, and anything else that puts elements in a
12
+ document. All of them need the same three rules, and nothing beyond them.
13
+
14
+ ## The three rules
15
+
16
+ **Strings go through attributes. Everything else goes through properties.** An
17
+ attribute is a string, so an array, an object or a function has to be assigned
18
+ to the element rather than written in the markup. Most frameworks do this for
19
+ you when the element has a matching property.
20
+
21
+ ```js
22
+ element.items = tabs
23
+ ```
24
+
25
+ **Events bind with `addEventListener`, and the name is the event's own.** Helix
26
+ events are `helix-before-open`, `helix-before-change`, `helix-select` and so on:
27
+ lowercase,
28
+ hyphenated, no camel-case variant. Whatever your framework's `on*` syntax is, it
29
+ should end up passing that string through.
30
+
31
+ ```js
32
+ element.addEventListener('helix-before-change', (event) => console.log(event.detail))
33
+ ```
34
+
35
+ **A declared `value`, `open`, `checked` or `active` is a claim of ownership.**
36
+ The element announces the change and applies none of it, and waits to be told.
37
+ Use `default-value`, `default-open` and their siblings for a control that owns
38
+ its own state. This is the rule most likely to look like a bug, and
39
+ [State and control](./state-and-control) is the page about it.
40
+
41
+ ## What each one needs
42
+
43
+ | Framework | Notes |
44
+ |---|---|
45
+ | Solid | Sets properties on custom elements by default. Use `prop:` to force one and `attr:` to force the other. `on:helix-change` binds the literal name |
46
+ | Qwik | Works as JSX. Bind with `document:on…` or a ref, since a plain `on*` prop is normalized |
47
+ | Lit | `.prop=`, `?bool=` and `@helix-before-change=` are already the right three channels |
48
+ | Ember | Angle-bracket invocation passes attributes only, so set properties from a modifier |
49
+ | Alpine, htmx | Markup and attributes only, which reaches every component. Refusing a change needs script |
50
+
51
+ Nothing here is tested by us, which is the honest reason it is one page. If your
52
+ framework needs something the three rules above do not cover, that is worth
53
+ telling us: it is either a gap in this page or a gap in the library.
54
+
55
+ ## Typings
56
+
57
+ `@fusion.dev/helix/jsx` is the base for anything JSX-shaped, and Svelte's own
58
+ typings are built from it. If your framework needs a different shape, the
59
+ manifest ships as `@fusion.dev/helix/custom-elements.json` and describes every tag,
60
+ prop, event and part, which is what all of ours are generated from.
61
+
62
+ ## Registration
63
+
64
+ ```ts
65
+ import '@fusion.dev/helix'
66
+ ```
67
+
68
+ Once, anywhere that runs in the browser. See [Installation](./installation) for
69
+ the stylesheets, which are the same in every framework and are the thing most
70
+ often left out.
@@ -0,0 +1,102 @@
1
+ ---
2
+ description: The top layer, what dismisses what, and where focus goes.
3
+ section: Essentials
4
+ title: Overlays and layering
5
+ ---
6
+
7
+ # Overlays and layering
8
+
9
+ `Dialog`, `Drawer`, `Popover`, `Menu`, `Select`, `Combobox`, `Tooltip`,
10
+ `Megamenu`, `Speed Dial`, `Command` and `Tour` are all overlays, and they all
11
+ work the same way underneath. Knowing that way saves you from most of the
12
+ problems overlays cause.
13
+
14
+ ## They render in the top layer
15
+
16
+ Every one uses the popover API, so the browser draws it in the top layer: above
17
+ everything, regardless of `z-index`, `overflow: hidden`, `transform` or a
18
+ stacking context somewhere up the tree.
19
+
20
+ **This is the fix for the oldest bug in the category.** A menu inside a
21
+ scrolling panel does not get clipped. A tooltip inside a `transform`ed card does
22
+ not get trapped in its stacking context. There is no portal to configure and no
23
+ `appendTo` to pass, because the platform is doing it.
24
+
25
+ The consequence for your CSS: **do not try to raise an overlay with `z-index`.**
26
+ It has no effect on a top-layer element, and reaching for it means something
27
+ else is wrong.
28
+
29
+ ## Positioning is JavaScript
30
+
31
+ Placement uses `@floating-ui/dom` rather than CSS anchor positioning: flip when
32
+ there is no room below, shift to stay on screen, size to the available space.
33
+ Anchor positioning would be the platform answer and is not yet available
34
+ everywhere the rest of the library's requirements are. See [Browser
35
+ support](./browser-support).
36
+
37
+ That means an overlay's position is recomputed while it is open, on scroll and
38
+ resize, and stops when it closes.
39
+
40
+ ## Dismissal
41
+
42
+ Light dismiss is the platform's: click outside, or press `Escape`. What Helix
43
+ adds is making that correct across shadow boundaries.
44
+
45
+ **`document.activeElement` stops at the outermost host.** Focus inside a shadow
46
+ tree is reported as the element containing that tree, so a naive "is focus still
47
+ inside me" check never matches and closes the overlay the moment anything inside
48
+ it is focused. Every dismissal path here walks through the roots instead.
49
+
50
+ **Nesting works because lifecycle events do not bubble.** `helix-before-open` and
51
+ `helix-before-close` are neither bubbling nor composed, so a menu closing inside a
52
+ dialog is not the dialog closing. `Escape` closes the innermost overlay and
53
+ leaves the rest.
54
+
55
+ ## Focus
56
+
57
+ A modal overlay traps `Tab` inside itself while open and returns focus to
58
+ whatever opened it. `Dialog` and `Drawer` do this; `Popover` does it in its
59
+ modal mode and not otherwise.
60
+
61
+ **A tooltip never takes focus**, which is the one to remember: it is
62
+ described-by rather than interactive, so it cannot contain a button. If you want
63
+ something with a button in it, that is a `Popover`.
64
+
65
+ ## Refusing to close
66
+
67
+ The close is announced before it happens:
68
+
69
+ ```ts
70
+ dialog.addEventListener('helix-before-close', (event) => {
71
+ if (unsaved) {
72
+ event.preventDefault()
73
+ }
74
+ })
75
+ ```
76
+
77
+ That covers `Escape`, the backdrop click and the close button together, because
78
+ all three go through the same request. See [State and
79
+ control](./state-and-control).
80
+
81
+ ## Entry and exit animation
82
+
83
+ Overlays animate in with `@starting-style` and out with a transition on the
84
+ `overlay` and `display` properties, which is what allows an exit animation on an
85
+ element the browser is about to remove from the top layer. Every one answers
86
+ `prefers-reduced-motion`.
87
+
88
+ ## Which one to reach for
89
+
90
+ | | |
91
+ |---|---|
92
+ | `Dialog` | A task that blocks the page |
93
+ | `Drawer` | The same, from an edge, usually larger |
94
+ | `Popover` | A surface anchored to a control, possibly interactive |
95
+ | `Tooltip` | A description of a control. Never interactive |
96
+ | `Menu` | A list of commands |
97
+ | `Select`, `Combobox` | A value chosen from a list, in a form |
98
+ | `Command` | A palette over the whole application |
99
+ | `Megamenu` | Navigation, anchored to a header |
100
+
101
+ A `Dialog` used for a tooltip's job traps focus for no reason. A `Tooltip` used
102
+ for a popover's job silently drops the button you put in it.