@codenhub/styles 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +7 -7
  2. package/dist/aesthetics/chunky-tile.css +1 -1
  3. package/dist/aesthetics/cyber.css +1 -1
  4. package/dist/aesthetics/glass.css +1 -1
  5. package/dist/aesthetics/index.css +1 -1
  6. package/dist/aesthetics/neobrutalism.css +1 -1
  7. package/dist/aesthetics/pixel.css +1 -1
  8. package/dist/aesthetics/sketch.css +1 -1
  9. package/dist/components.css +1 -1
  10. package/dist/index.css +1 -1
  11. package/dist/native.css +1 -1
  12. package/dist/palette.css +1 -1
  13. package/dist/theme.css +1 -1
  14. package/dist/tw/aesthetics/chunky-tile.css +39 -39
  15. package/dist/tw/aesthetics/cyber.css +16 -11
  16. package/dist/tw/aesthetics/glass.css +235 -10
  17. package/dist/tw/aesthetics/index.css +2 -1
  18. package/dist/tw/aesthetics/neobrutalism.css +23 -11
  19. package/dist/tw/aesthetics/pixel.css +18 -8
  20. package/dist/tw/aesthetics/sketch.css +14 -7
  21. package/dist/tw/box.css +49 -27
  22. package/dist/tw/components/button.css +35 -14
  23. package/dist/tw/components/feedback.css +59 -56
  24. package/dist/tw/components/form.css +200 -126
  25. package/dist/tw/components/index.css +3 -7
  26. package/dist/tw/components/loader.css +4 -5
  27. package/dist/tw/components/surface.css +71 -68
  28. package/dist/tw/components/tooltip.css +75 -41
  29. package/dist/tw/elevation.css +14 -22
  30. package/dist/tw/index.css +15 -19
  31. package/dist/tw/intent.css +31 -24
  32. package/dist/tw/native.css +15 -5
  33. package/dist/tw/palette.css +8 -5
  34. package/dist/tw/presentation.css +16 -30
  35. package/dist/tw/reset.css +21 -28
  36. package/dist/tw/surface.css +11 -12
  37. package/dist/tw/theme.css +58 -86
  38. package/dist/tw/utilities/content.css +268 -199
  39. package/dist/tw/utilities/layout.css +15 -25
  40. package/docs/accessibility.md +50 -33
  41. package/docs/changelog/0.6.0.md +52 -0
  42. package/docs/changelog/index.md +1 -0
  43. package/docs/concepts.md +6 -6
  44. package/docs/index.md +9 -0
  45. package/docs/integrating/nextjs.md +1 -1
  46. package/docs/integrating/tailwind.md +14 -6
  47. package/docs/setup.md +6 -6
  48. package/docs/usage/aesthetics.md +35 -8
  49. package/docs/usage/buttons.md +1 -1
  50. package/docs/usage/composing.md +1 -1
  51. package/docs/usage/content-and-layout.md +12 -11
  52. package/docs/usage/customizing.md +8 -9
  53. package/docs/usage/feedback.md +5 -3
  54. package/docs/usage/forms.md +51 -35
  55. package/docs/usage/theming.md +39 -40
  56. package/docs/usage/tooltips.md +37 -17
  57. package/llms-full.txt +331 -183
  58. package/llms.txt +6 -0
  59. package/package.json +5 -5
@@ -14,14 +14,11 @@ Layout helpers use the shared `--layout-gap` token. `.tight` sets it to `0.5rem`
14
14
  - `.stack` is a vertical flex stack.
15
15
  - `.cluster` is a wrapping horizontal flex row; `.between` adds `space-between` alignment.
16
16
  - `.auto-grid` is a responsive auto-fit grid using `--layout-grid-min`.
17
- - `.tight` and `.loose` set the shared gap to `0.5rem` or `1.5rem` on views, stacks, clusters, and auto-grids.
18
17
  - `.section` adds responsive block padding and an inline gutter.
19
18
  - `.section-content` centers content at `--container-max`; `.narrow` and `.wide` select the corresponding container tokens.
20
19
  - `.divider` is horizontal; `.vertical` makes it self-stretch vertically. It takes intent, but presentation classes do not affect it.
21
20
  - `.visually-hidden` hides content visually while keeping it in the accessibility tree, using the standard clip-based recipe. Not named `.sr-only`: on a `/tw` entry the consumer's own Tailwind build already ships a real `.sr-only`, and a same-named utility of this package's own would silently collide with it in the cascade — the same reason `.data-table` is not named `.table`.
22
21
 
23
- The removed `--layout-stack-gap` and `--layout-cluster-gap` tokens have no compatibility aliases.
24
-
25
22
  ## Content
26
23
 
27
24
  | Class | Purpose | Intent affects |
@@ -48,24 +45,24 @@ A table draws three kinds of line, and `.ruled` and `.ruleless` move all of them
48
45
 
49
46
  ```html
50
47
  <table class="data-table">
51
- Head and foot boundaries only
48
+ <!-- Head and foot boundaries only -->
52
49
  </table>
53
50
  <table class="data-table ruled">
54
- Every row separated
51
+ <!-- Every row separated -->
55
52
  </table>
56
53
  <table class="data-table ruleless">
57
- No lines inside the table at all
54
+ <!-- No lines inside the table at all -->
58
55
  </table>
59
56
  ```
60
57
 
61
- The boundaries under a head and above a foot separate the parts of a table from each other, so they are drawn by default where the row rules are not. Rules between body rows used to arrive on their own, because the component's published edge default is `edged` and the edge axis wrote the rules along with the boundary — so whether you got them depended on a registry default rather than on anything in your markup.
58
+ The boundaries under a head and above a foot separate the parts of a table from each other, so they are drawn by default where the row rules are not. Rules between body rows are asked for by `.ruled` or `--ui-rule`, never by the edge axis, so whether you get them depends on your markup and not on a default you cannot see.
62
59
 
63
60
  `--ui-rule` is the token behind both classes, so a container can set the answer for a whole region without classing each table:
64
61
 
65
62
  ```html
66
63
  <section style="--ui-rule: 100%">
67
64
  <table class="data-table">
68
- Ruled by the region
65
+ <!-- Ruled by the region -->
69
66
  </table>
70
67
  </section>
71
68
  ```
@@ -93,10 +90,10 @@ This is the one place intent cascades, because a table's rows are parts of the t
93
90
 
94
91
  ```html
95
92
  <table class="data-table success soft edged">
96
- ...
93
+ <!-- ... -->
97
94
  </table>
98
- <table class="data-table">
99
- A ghost table: rules and type, no header plate
95
+ <table class="data-table ghost">
96
+ <!-- A ghost table: rules and type, no header plate -->
100
97
  </table>
101
98
  <kbd class="kbd primary solid">Ctrl</kbd>
102
99
  <kbd class="kbd primary soft edged">Shift</kbd>
@@ -131,6 +128,10 @@ Use `.table-wrap` around `.data-table` when table width may exceed its container
131
128
  </div>
132
129
  ```
133
130
 
131
+ A table takes the surface corner, like a card beside it. Nothing clips it: the four corner cells take the corners, so a `<caption>` sits above the table untouched in every engine. A cell that spans rows down into a bottom corner leaves that corner square.
132
+
133
+ `.table-wrap` scrolls, and a scroll container clips whatever paints outside it. Under an aesthetic that draws a halo -- cyber's glow -- the wrapper pads itself by the halo's reach and pulls itself out by the same amount, so the glow fits inside it and the table stays where it would be. The wrapper then reaches that far past its parent, which only shows when the parent is flush with the viewport. Depth shadows, such as a `.raised` table's, are not given room and are still cut. The wrapper can only make room for a halo it is inside, so put the aesthetic class on `.table-wrap` or an ancestor rather than on a wrapped table alone: `<table class="data-table cyber">` inside a plain wrapper glows, and the wrapper cuts the glow. Leave the wrapper off a table that can never outgrow its container; keep it wherever one can, or a wide table widens the whole page.
134
+
134
135
  ## Surfaces
135
136
 
136
137
  | Class | Purpose |
@@ -54,6 +54,7 @@ Components read each with a fallback, so leaving them unset produces the default
54
54
  | `--ui-border-max` | Ceiling on computed edge width. | `100px` |
55
55
  | `--ui-ink` | Neutral line color when no intent is set. | `--color-border` |
56
56
  | `--ui-control-ink` | Neutral line color of fields, selects, checkboxes, radios, and switches when no intent is set. | `--ui-ink`, then `--color-control-border` |
57
+ | `--ui-rule-ink` | Neutral line color of a table's frame and rules when no intent is set. | `--ui-ink`, then `--color-border` |
57
58
  | `--ui-rule` | Lines inside a table. Unset draws the head and foot boundaries only; `0%` draws none; `100%` rules every row. | unset |
58
59
  | `--ui-shadow-x` | Shadow horizontal offset. | `0px` |
59
60
  | `--ui-shadow-y` | Shadow vertical offset. | `0px` |
@@ -72,9 +73,9 @@ Components read each with a fallback, so leaving them unset produces the default
72
73
  | `--ui-shadow-edge` | Declared, even empty, when the shadow is the element's edge rather than its depth. Read for presence. | Unset |
73
74
  | `--ui-elevation` | Unitless multiplier over shadow geometry. | `1` |
74
75
  | `--ui-surface-shadow` | Complete multi-layer shadow accepted by surfaces only. | Unset |
75
- | `--ui-surface-ground` | Ground a surface sits on. | `--color-background` |
76
- | `--ui-bg-alpha` | Multiplier over fill for translucent materials. | `1` |
77
- | `--ui-backdrop` | Backdrop filter accepted by surfaces only. | `none` |
76
+ | `--ui-surface-ground` | Ground a surface, or a default or `.soft` table, sits on. | `--color-background` |
77
+ | `--ui-bg-alpha` | Multiplier over every fill, `.solid` included. No shipped aesthetic sets it. | `1` |
78
+ | `--ui-backdrop` | Backdrop filter accepted by surfaces, and by a default or `.soft` table, only. | `none` |
78
79
  | `--ui-surface-image` | A painted layer over a surface's plate: any `background-image` value. Surfaces only. | `none` |
79
80
  | `--ui-hover-transform` | Transform while an interactive element is hovered. | `none` |
80
81
  | `--ui-clip` | Structural component silhouette. | `none` |
@@ -148,8 +149,8 @@ The shipped [aesthetic classes](./aesthetics.md) set material tokens for you. On
148
149
  | Token | Aesthetic | Purpose | Default |
149
150
  | --------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
150
151
  | `--neo-offset` | `.neobrutalism` | Hard shadow offset and distance the press travels. | `4px` |
151
- | `--glass-radius` | `.glass` | Control corner radius. | `0.75rem` |
152
- | `--glass-radius-surface` | `.glass` | Surface corner radius. | `1rem` |
152
+ | `--glass-radius` | `.glass`, `.glass-liquid` | Control corner radius. `1rem` under `.glass-liquid`. | `0.75rem` |
153
+ | `--glass-radius-surface` | `.glass`, `.glass-liquid` | Surface corner radius. `1.5rem` under `.glass-liquid`. | `1rem` |
153
154
  | `--pixel-unit` | `.pixel` | One pixel of the grid. The corner cut and the ring are one. | `4px` |
154
155
  | `--font-pixel` | `.pixel` | Consumer-supplied pixel font stack. | Falls back to the monospace stack |
155
156
  | `--tile-radius` | `.chunky-tile` | Corner radius, on controls and surfaces alike. | `0.75rem` |
@@ -186,7 +187,7 @@ Every aesthetic's knobs reach the same way: each knob is _read_ with its default
186
187
 
187
188
  Knobs and material tokens reach in opposite directions, and telling them apart matters. A knob resolves once at the aesthetic's own class and inherits down already-resolved, so it reaches the aesthetic **from above**: setting it on the element or any ancestor works, but setting it on a _descendant_ of the aesthetic class changes nothing — the value has already been read. `--ui-radius` and the other material tokens above are read fresh by each component as it draws, so they reach **at or below** the element carrying them, which is how you round one card differently from its neighbours without touching the knob at all.
188
189
 
189
- Shipped aesthetics set the shared material tokens above. Every shipped aesthetic sets `--ui-ink` for its neutral line. `.glass` and `.chunky-tile` also set `--ui-control-ink`, so their controls keep a heavier line than their surfaces; the others clear it and their controls take `--ui-ink`. Intent classes still override the intent slots on each component.
190
+ Shipped aesthetics set the shared material tokens above. Every shipped aesthetic sets `--ui-ink` for its neutral line. Every shipped aesthetic also sets `--ui-control-ink`: `.glass` and `.chunky-tile` to a line of their own, heavier than their surfaces', and the others to `--color-control-border`, since their ink is the primary colour in both themes. `.glass` sets `--ui-rule-ink` too, so a table's lines read on its plate; the others clear it. Intent classes still override the intent slots on each component.
190
191
 
191
192
  The same knobs tune each aesthetic's [solo class](./aesthetics.md#solo-classes), which reads them on its own element.
192
193
 
@@ -213,7 +214,7 @@ Naming: `--palette-<intent>-<presentation>-<slot>`, where `<intent>` is one of t
213
214
  - A ground qualifier (`page` or `subtle`) inserted before the slot, present only on `.soft`/`.ghost`'s `bg` and `edge` -- `.solid` is ground-independent at full fill, and `fg` never varies by ground. No qualifier means the `transparent` ground (`.badge`/`.btn`'s ground): `--palette-success-soft-bg`. `page` is `.alert`'s ground (`--color-background`): `--palette-success-soft-page-bg`. `subtle` is `.pre`/`.code`/`.kbd`/`.tooltip-bubble`'s ground (that intent's own `--intent-subtle`): `--palette-success-soft-subtle-bg`.
214
215
  - A `-hover` suffix on `bg` and `edge` only -- `fg` never changes on hover: `--palette-success-soft-page-bg-hover`.
215
216
 
216
- Dark values are scoped the same way `@codenhub/styles`' own explicit theme override is: `.dark`, `.theme-dark`, and `[data-theme="dark"]`, on the element or an ancestor. There is no `prefers-color-scheme` fallback in this file -- a consumer reaching for a standalone palette is already handling its own theme switching, so the OS-preference case is left to `@codenhub/styles`' own `light-dark()`-based tokens when that package is actually in use.
217
+ Light values sit on `:root` and dark values are scoped the same way `@codenhub/styles`' own explicit theme override is: `.dark`, `.theme-dark`, and `[data-theme="dark"]`, on the element or an ancestor. The light values also answer `.light`, `.theme-light`, and `[data-theme="light"]`, so the nearest explicit theme wins at any depth -- a light subtree nested inside a dark one reads the light values. There is no `prefers-color-scheme` fallback in this file -- a consumer reaching for a standalone palette is already handling its own theme switching, so the OS-preference case is left to `@codenhub/styles`' own `light-dark()`-based tokens when that package is actually in use.
217
218
 
218
219
  Three more tokens sit alongside the intent cells for the same reason, without an intent, presentation, ground, or hover axis of their own: `--palette-border`, `--palette-surface`, and `--palette-text`, flat values for `--color-border`, `--color-surface`, and `--color-text` -- the resting colors anything that is not intent-colored needs (a container's border, a dialog's background, its text), which an optional-peer consumer needs just as much as the intent cells.
219
220
 
@@ -223,6 +224,4 @@ Component classes may define scoped implementation variables such as `--surface-
223
224
 
224
225
  Consumers should customize broad behavior through color, foundation, motion, elevation, radius, layout, and focus tokens first. The only supported component-scoped input is `--progress-value` on the `.progress` element, because consumers must provide a value such as `64%`.
225
226
 
226
- `--layout-gap` replaces the removed `--layout-stack-gap` and `--layout-cluster-gap` tokens. No compatibility aliases are provided.
227
-
228
227
  Next: [Aesthetics](./aesthetics.md) covers each shipped aesthetic's complete look and its documented exceptions.
@@ -33,7 +33,7 @@ Activity indicator modifier classes compose with `.loader`:
33
33
  | `.bars-wave` | Three vertical bars scaling in a wave. |
34
34
  | `.pulse-ring` | Two concentric rings pulsing outward. |
35
35
 
36
- These variants retain their embedded SVG animations normally. Under `prefers-reduced-motion: reduce`, loader CSS substitutes a corresponding static mask. This fallback is part of focused loader, button, and components imports; it does not depend on the global reset. See [Accessibility](../accessibility.md).
36
+ These variants retain their embedded SVG animations normally. Under `prefers-reduced-motion: reduce`, loader CSS substitutes a corresponding static mask. This fallback ships wherever the loader does -- the complete, native, and `/components` entrypoints, `/tw/components`, and `/tw/loader` -- and does not depend on the global reset. `/tw/button` does not include the loader; pair it with `/tw/loader`. See [Accessibility](../accessibility.md).
37
37
 
38
38
  Loader size modifiers:
39
39
 
@@ -43,7 +43,7 @@ Loader size modifiers:
43
43
  | `.sm` | Small (`1.25rem`). |
44
44
  | `.lg` | Large (`2.25rem`). |
45
45
 
46
- Alerts, badges, progress bars, skeletons, and loaders accept `.primary`, `.secondary`, `.success`, `.warning`, `.destructive`, and `.info`. Without an intent, they use the text palette, except `.loader`, which keeps `currentColor` so it matches whatever content surrounds it.
46
+ Alerts, badges, progress bars, skeletons, and loaders accept `.primary`, `.secondary`, `.success`, `.warning`, `.destructive`, and `.info`. Without an intent, alerts and badges use the text palette, while the indicators -- `.loader`, `.skeleton`, and `.progress` -- take `currentColor`, so they match whatever content surrounds them.
47
47
 
48
48
  Skeletons, loaders, and dividers are indicators: they read intent but ignore fill and edge [presentation](./composing.md#presentation). `.progress` is not one of them — its track reads both, `.soft.edgeless` by default; only the moving value fill stays intent-colored at full strength regardless of presentation. `.solid` is unsupported on the track, and `.ghost.edgeless` renders but is discouraged, since neither leaves the track a visible frame. See [Composing → Component axis reference](./composing.md#component-axis-reference) for the full table.
49
49
 
@@ -51,7 +51,9 @@ Skeletons, loaders, and dividers are indicators: they read intent but ignore fil
51
51
  <span class="loader success" aria-hidden="true"></span> <span class="skeleton info"></span>
52
52
  ```
53
53
 
54
- The package ships no icon of its own: `.alert` is already `flex`, so an icon dropped in as a child is spaced from the message by its `gap-3` with nothing to trigger. `.alert-icon` only sizes and aligns whatever icon element you provide — an inline `<svg>`, an `<img>`, or a class from an icon set such as `@codenhub/icons` — the same contract [`.input-group`](./forms.md#icons) documents.
54
+ The package ships no icon of its own: `.alert` is already `flex`, so an icon dropped in as a child is spaced from the message by its `gap-3`. `.alert-icon` only sizes and aligns whatever icon element you provide — an inline `<svg>`, an `<img>`, or a class from an icon set such as `@codenhub/icons` — the same contract [`.input-group`](./forms.md#icons) documents.
55
+
56
+ `.alert-icon` sizes the icon to `1.25rem`. A `@codenhub/icons` glyph sizes itself in an unlayered rule that no class of this package can outrank, so `.alert-icon` sets that set's `--ic-size` as well; an icon set with a different prefix keeps its own size unless you set its knob. Resize an alert icon with an ordinary rule of your own, and set `--ic-size` along with `width` and `height` for a `@codenhub/icons` glyph.
55
57
 
56
58
  Alerts and badges read the shared [presentation](./composing.md#presentation) classes. Without one they use a tinted surface, intent-colored text, and a mixed intent border -- except a neutral `.alert` (no named intent), which rests untinted instead, the same no-named-intent carve-out `.card.soft` and `.panel` use: a plain `--color-foreground` plate rather than 12% of near-black ink over the page. A named intent (`.alert.success`, `.alert.destructive`, and so on) keeps its ordinary tint. `.badge` is unaffected either way -- it has no border to fall back on at rest, so untinting it would make a neutral badge disappear rather than read quieter.
57
59
 
@@ -6,55 +6,69 @@ order: 6
6
6
 
7
7
  # Forms
8
8
 
9
- | Class or Selector | Purpose |
10
- | --------------------------------------------------------------------- | ------------------------------------------------------------------------ |
11
- | `.field` | Vertical field wrapper. |
12
- | `.label` | Form label text. |
13
- | `.hint` | Secondary helper text. |
14
- | `.hint.destructive` | Helper text with destructive intent. |
15
- | `.surface` | Shared public container composition utility. |
16
- | `.text-control` | Shared public text-control composition utility. |
17
- | `.ipt` | Input control styling. |
18
- | `.input-group` | Wrapper that owns the field box so a control can carry an icon or affix. |
19
- | `.textarea` | Textarea control styling. |
20
- | `.select` | Select control styling. |
21
- | `input[type="checkbox"].checkbox` | Custom checkbox control styling. |
22
- | `input[type="radio"].radio` | Custom radio control styling. |
23
- | `input[type="checkbox"].switch` | Custom switch control styling. |
24
- | `[aria-invalid="true"]` on controls | Destructive border and focus color. |
25
- | `[disabled]`, `[aria-disabled="true"]`, `[data-disabled]` on controls | Disabled styling. |
9
+ | Class or Selector | Purpose |
10
+ | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
11
+ | `.field` | Vertical field wrapper. |
12
+ | `.label` | Form label text. |
13
+ | `.hint` | Secondary helper text. |
14
+ | `.hint.destructive` | Helper text with destructive intent. |
15
+ | `.surface` | Shared public container composition utility. |
16
+ | `.text-control` | Shared public text-control composition utility. |
17
+ | `.ipt` | Input control styling. |
18
+ | `.input-group` | Wrapper that owns the field box so a control can carry an icon or affix. |
19
+ | `.unadorned` | Removes the browser's own field decorations, on the control or on its `.input-group`. |
20
+ | `.textarea` | Textarea control styling. |
21
+ | `.select` | Select control styling. With `multiple` or a `size` above 1 it is a list box: no chevron, as tall as its rows. |
22
+ | `input[type="checkbox"].checkbox` | Custom checkbox control styling. |
23
+ | `input[type="radio"].radio` | Custom radio control styling. |
24
+ | `input[type="checkbox"].switch` | Custom switch control styling. |
25
+ | `[aria-invalid="true"]` on controls | Destructive border and focus color. |
26
+ | `[disabled]`, `[aria-disabled="true"]`, `[data-disabled]` on controls | Disabled styling. |
26
27
 
27
28
  ## Icons
28
29
 
29
- No icon is shipped for text inputs — that's what this section covers. It used to paint one per input type as a `background-image` — a `data:` URI, which cannot read `currentColor` or a custom property, so every glyph shipped a light and a dark copy and a theme selector chose between them. That put icon artwork, and the package's only theme-by-selector branch, inside a CSS-only design system whose glyphs did not fit every aesthetic. Icons for `.input-group` are the consumer's to choose now.
30
-
31
- This is separate from the toggle marks below, which stay small, fixed, built-in glyphs. `.alert`'s icon followed the same path since — see [Feedback → Icon](./feedback.md#example) — the package no longer paints one there either.
30
+ The package ships no icon for text inputs. Icons for `.input-group` are the consumer's to choose: artwork painted as a `data:` URI could not read `currentColor` or a custom property, so it could not follow the theme or fit every aesthetic. The toggle marks below are the exception: small, fixed, built-in glyphs. `.alert` likewise takes a consumer-supplied icon; see [Feedback](./feedback.md#example).
32
31
 
33
32
  `.input-group` is the wrapper for a control that carries one. It owns the field box — border, radius, fill, focus ring, invalid and disabled state — and the control inside it goes flush, so the boundary is drawn once. Any icon element works, placed before the control for a leading icon or after it for a trailing one: an inline `<svg>`, an `<img>`, or a class from an icon set such as `@codenhub/icons`.
34
33
 
35
34
  ```html
36
- <label class="field">
37
- <span class="label">Email</span>
35
+ <div class="field">
36
+ <label class="label" for="contact-email">Email</label>
38
37
  <div class="input-group">
39
38
  <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" aria-hidden="true">
40
39
  <rect width="20" height="16" x="2" y="4" rx="2" />
41
40
  <path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" />
42
41
  </svg>
43
- <input class="ipt" type="email" />
42
+ <input class="ipt" type="email" id="contact-email" />
44
43
  </div>
45
- </label>
46
- <label class="field">
47
- <span class="label">Search</span>
44
+ </div>
45
+ <div class="field">
46
+ <label class="label" for="site-search">Search</label>
48
47
  <div class="input-group">
49
- <input class="ipt" type="search" />
48
+ <input class="ipt" type="search" id="site-search" />
50
49
  <i class="ic-search" aria-hidden="true"></i>
51
50
  </div>
52
- </label>
51
+ </div>
53
52
  ```
54
53
 
55
54
  The group reads intent and both [presentation](./composing.md#presentation) axes the same way a lone `.ipt` does — `.input-group.soft.edgeless` is the sunk variant, `.input-group.primary` colors the boundary — and follows whichever [aesthetic](./aesthetics.md) is in scope. `aria-invalid` or `disabled` on the control inside propagates to the group; so does either on the group itself. Focus is shown with `:focus-within`, so unlike a lone field the ring also appears on a mouse click.
56
55
 
57
- Non-icon input types keep the browser's native `date` and `datetime-local` pickers. WebKit's native `search` decorations are still suppressed on `.text-control` to keep them from overlapping the value.
56
+ Text controls keep the browser's own decorations: the date and time picker button, a number field's spin buttons, and a search field's clear button and magnifier. Each one is also behaviour -- the picker button is the only pointer route to the picker, and the clear button empties the field.
57
+
58
+ `.unadorned` removes all of them, for a field whose adornments you draw yourself. Put it on the control, or on its `.input-group` to reach the control inside. Removing a decoration removes what it did, so wire the behaviour back to your own adornment: `input.showPicker()` for a picker, `stepUp()`/`stepDown()` for spin buttons, and setting `value` to `""` and dispatching an `input` event for a clear button. Keyboard entry keeps working either way. Firefox exposes no way to remove its date picker button, so it stays there.
59
+
60
+ ```html
61
+ <div class="field">
62
+ <label class="label" for="query">Search</label>
63
+ <div class="input-group unadorned">
64
+ <i class="ic-search" aria-hidden="true"></i>
65
+ <input class="ipt" type="search" id="query" />
66
+ <button type="button" class="btn icon ghost p-xs" aria-label="Clear search">
67
+ <i class="ic-x" aria-hidden="true"></i>
68
+ </button>
69
+ </div>
70
+ </div>
71
+ ```
58
72
 
59
73
  The slot works the same way for a text affix — a URL host, a currency symbol, a unit — not just an icon:
60
74
 
@@ -91,6 +105,8 @@ All three toggles rest at `.soft`. Presentation decides the _unchecked_ plate; c
91
105
 
92
106
  A checked checkbox and a checked switch cut their mark out of that pinned plate. A checked radio thickens its ring to twice the resting line and takes the intent whole on it, and the dot follows the same pinned plate, so `.radio.soft` and `.radio.solid` render the same filled circle once checked — presentation only changes what the radio looks like before it's picked.
93
107
 
108
+ A checkbox whose `indeterminate` property is set -- a group some of whose items are checked -- takes the same pinned plate with a dash for its mark, and keeps the dash while it is also `checked`, as the browser's own checkbox does. The property is set from script (`input.indeterminate = true`); there is no attribute for it.
109
+
94
110
  ## Text controls
95
111
 
96
112
  Text controls also take intent, which colors the resting border and the focus-visible border, and both [presentation](./composing.md#presentation) axes. `.ghost` fills nothing, `.soft` takes `12%`, and `.solid` takes `20%` — quiet enough that typed text still reads on it, and ordered so the louder name draws the stronger tint. None of them touches the line.
@@ -123,11 +139,11 @@ The boundary is the edge axis's, and it splits the same way. A container's `.edg
123
139
  ## Example
124
140
 
125
141
  ```html
126
- <label class="field">
127
- <span class="label">Email</span>
128
- <input class="ipt" type="email" aria-invalid="true" aria-describedby="email-error" />
142
+ <div class="field">
143
+ <label class="label" for="email">Email</label>
144
+ <input class="ipt" type="email" id="email" aria-invalid="true" aria-describedby="email-error" />
129
145
  <span class="hint destructive" id="email-error">Enter a valid email.</span>
130
- </label>
146
+ </div>
131
147
  <label style="display: flex; gap: 0.5rem; align-items: center">
132
148
  <input type="checkbox" class="checkbox success" />
133
149
  <span>Accept terms</span>
@@ -137,9 +153,9 @@ The boundary is the edge axis's, and it splits the same way. A container's `.edg
137
153
  <span>Standard plan</span>
138
154
  </label>
139
155
  <label style="display: flex; gap: 0.5rem; align-items: center">
140
- <input type="checkbox" class="switch destructive" />
156
+ <input type="checkbox" class="switch destructive" role="switch" />
141
157
  <span>Enable</span>
142
158
  </label>
143
159
  ```
144
160
 
145
- Use labels, `type`, validation logic, `aria-describedby`, and error message relationships; see [Accessibility](../accessibility.md).
161
+ Keep a hint or error message outside the `<label>` and point `aria-describedby` at it, so it is announced as a description rather than read as part of the field's name. Give a `.switch` `role="switch"`. Use labels, `type`, validation logic, `aria-describedby`, and error message relationships; see [Accessibility](../accessibility.md).
@@ -72,15 +72,15 @@ Every row is an override point: set it on `:root` to retheme globally, or on any
72
72
 
73
73
  Intent classes set these seven slots; components read them. Because every supporting component reads the same seven names, an intent works on every component that supports intent, and a custom intent needs no component changes.
74
74
 
75
- | Token | Purpose |
76
- | ------------------- | ------------------------------------------------------------------------------------- |
77
- | `--intent-color` | The intent's base color. What a fill is made of. |
78
- | `--intent-contrast` | Readable color on top of a filled `--intent-color`. |
79
- | `--intent-hover` | The intent's hovered base color. |
80
- | `--intent-strong` | The intent printed on a page. Text, icon, or border wherever the fill is not full. |
81
- | `--intent-subtle` | Low-emphasis tone. Tinted surfaces and tracks. Neutral resolves to `--color-surface`. |
82
- | `--intent-border` | Line color. Resolves to `--color-border` when no intent is set. |
83
- | `--intent-fill-max` | How far a fill of this intent is allowed to go. `100%` for every intent but neutral. |
75
+ | Token | Purpose |
76
+ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
77
+ | `--intent-color` | The intent's base color. What a fill is made of. |
78
+ | `--intent-contrast` | Readable color on top of a filled `--intent-color`. |
79
+ | `--intent-hover` | The intent's hovered base color. |
80
+ | `--intent-strong` | The intent printed on a page. Text, icon, or border wherever the fill is not full. |
81
+ | `--intent-subtle` | Low-emphasis tone. Tinted surfaces and tracks. Neutral resolves to `--color-surface`. |
82
+ | `--intent-border` | Line color. With no intent it resolves to the aesthetic's `--ui-ink`, then `--color-border`; text controls and toggles read `--ui-control-ink`, then `--ui-ink`, then `--color-control-border`, and tables read `--ui-rule-ink` first. |
83
+ | `--intent-fill-max` | How far a fill of this intent is allowed to go. `100%` for every intent but neutral. |
84
84
 
85
85
  `--intent-color` and `--intent-strong` are two ends of the same family and not interchangeable. The base is a ground, chosen to be filled with; the strong tone is chosen to be read against a page. A `.soft` or `.ghost` component prints the strong one, which is why an unfilled warning reads in amber-800 rather than the amber-600 its solid sibling is filled with.
86
86
 
@@ -92,7 +92,7 @@ The strong tone has to stay recognisably its own hue, not just readable. On a da
92
92
 
93
93
  The foreground is gated by the plate rather than tied to the fill. `--intent-contrast` is the ink for a _full_ fill, so printing it at full strength on a fifth of one puts white on light grey. Contrast ink therefore appears only once the fill is past halfway and reaches full at a full fill; below that a component prints `--intent-strong`, the tone chosen to be read.
94
94
 
95
- That means a capped neutral takes the strong tone whole rather than a fraction of the way toward the page. Tying it to the fill instead cost a neutral `.solid` button four points of contrast and a neutral tooltip nearly ten. A state that lifts the cap on purpose — a checked checkbox is filled with its own intent by definition — reaches a full fill and gets the contrast whole.
95
+ So a capped neutral takes the strong tone whole rather than a fraction of the way toward the page. A state that lifts the cap on purpose — a checked checkbox is filled with its own intent by definition — reaches a full fill and gets the contrast whole.
96
96
 
97
97
  Set them directly to define an intent this package does not ship. **State every slot**, including `--intent-fill-max`: components reset all seven to the neutral values, so a slot your intent leaves out keeps neutral's — and an intent that omits the cap fills to 20% of its color instead of 100%.
98
98
 
@@ -120,35 +120,34 @@ Foundation tokens are not aliases for one color. They define layout, shape, moti
120
120
 
121
121
  The four `--elevation-*` values below are raw shadows for elements you style yourself. They are a separate thing from `.flat`/`.raised`/`.floating`, which scale a _component's own_ shadow geometry and default to no shadow at all until an aesthetic or one of those classes asks for depth — see [Composing → Elevation](./composing.md#elevation) for that system. The two share `none`/`sm`/`md`/`lg` naming for the same weight of depth, but one is a fixed value and the other multiplies shadow parts, so they will not always paint identically.
122
122
 
123
- | Token | Purpose |
124
- | -------------------------- | ----------------------------------------------------------------------- |
125
- | `--font-default` | Default app font family. |
126
- | `--container-narrow` | Narrow content width. |
127
- | `--container-max` | Default content width. |
128
- | `--container-wide` | Wide content width. |
129
- | `--layout-gutter` | Responsive inline page padding. |
130
- | `--layout-section-block` | Responsive section vertical padding. |
131
- | `--layout-gap` | Shared gap for `.view`, `.stack`, `.cluster`, and `.auto-grid`. |
132
- | `--layout-grid-min` | Minimum column width for `.auto-grid`. |
133
- | `--radius-small` | Small radius for compact utility surfaces such as code elements. |
134
- | `--radius-control` | Radius for buttons, inputs, tooltips, and compact UI. |
135
- | `--radius-surface` | Radius for cards, panels, and alerts. |
136
- | `--control-height` | Default minimum height for controls. |
137
- | `--border-width` | Default border width. |
138
- | `--elevation-color` | Shadow color the four elevations compose from. Heavier in dark themes. |
139
- | `--elevation-none` | No shadow. Use on an element that should read as flat against its page. |
140
- | `--elevation-sm` | A light shadow for your own surface-level elements, such as cards. |
141
- | `--elevation-md` | A shadow for your own raised elements, such as dropdowns and popovers. |
142
- | `--elevation-lg` | A shadow for your own full overlays, such as modal dialogs. |
143
- | `--focus-ring` | Focus-visible ring color. |
144
- | `--focus-ring-offset` | Focus-visible outline offset. Negative, so the ring lands on the edge. |
145
- | `--focus-ring-width` | Focus-visible outline/ring width. |
146
- | `--motion-duration-fast` | Fast transition for buttons and input interactions (`120ms`). |
147
- | `--motion-duration-normal` | Normal transition for layout and modals (`200ms`). |
148
- | `--motion-duration-slow` | Slow animation for skeleton loaders and progress bars (`400ms`). |
149
- | `--motion-ease` | Default easing curve. |
150
- | `--z-popover` | Popover/tooltip z-index. |
151
- | `--breakpoint-xs` | Extra-small Tailwind responsive breakpoint. |
152
- | `--breakpoint-2xl` | Extended large Tailwind responsive breakpoint. |
123
+ | Token | Purpose |
124
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
125
+ | `--font-default` | Default app font family. |
126
+ | `--container-narrow` | Narrow content width. |
127
+ | `--container-max` | Default content width. |
128
+ | `--container-wide` | Wide content width. |
129
+ | `--layout-gutter` | Responsive inline page padding. |
130
+ | `--layout-section-block` | Responsive section vertical padding. |
131
+ | `--layout-gap` | Shared gap for `.view`, `.stack`, `.cluster`, and `.auto-grid`. |
132
+ | `--layout-grid-min` | Minimum column width for `.auto-grid`. |
133
+ | `--radius-small` | Small radius for compact utility surfaces such as code elements. |
134
+ | `--radius-control` | Radius for buttons, inputs, tooltips, and compact UI. |
135
+ | `--radius-surface` | Radius for cards, panels, and alerts. |
136
+ | `--control-height` | Default minimum height for controls. |
137
+ | `--border-width` | Default border width. |
138
+ | `--elevation-color` | Shadow color the four elevations compose from. Heavier in dark themes. |
139
+ | `--elevation-none` | No shadow. Use on an element that should read as flat against its page. |
140
+ | `--elevation-sm` | A light shadow for your own surface-level elements, such as cards. |
141
+ | `--elevation-md` | A shadow for your own raised elements, such as dropdowns and popovers. |
142
+ | `--elevation-lg` | A shadow for your own full overlays, such as modal dialogs. |
143
+ | `--focus-ring` | Focus-visible ring color. |
144
+ | `--focus-ring-offset` | Focus-visible outline offset. Unset: the negative of the focused element's line width, so the ring lands on the edge under every aesthetic. |
145
+ | `--focus-ring-width` | Focus-visible outline/ring width. |
146
+ | `--motion-duration-fast` | Fast transition for a table row's hover (`120ms`). |
147
+ | `--motion-duration-normal` | Every component's colour, border, shadow, and press transitions, toggle marks, and the tooltip bubble (`200ms`). |
148
+ | `--motion-duration-slow` | Progress value changes, and the base of the skeleton and progress shimmer cycle (`400ms`). |
149
+ | `--motion-ease` | Default easing curve. |
150
+ | `--z-popover` | Popover/tooltip z-index. |
151
+ | `--breakpoint-xs` | Extra-small Tailwind responsive breakpoint. |
153
152
 
154
153
  Next: [Customizing](./customizing.md) covers presentation and material tokens — how much of an intent shows, and what a component is made of.
@@ -6,24 +6,25 @@ order: 8
6
6
 
7
7
  # Tooltips
8
8
 
9
- | Class or Attribute | Purpose |
10
- | -------------------------------------- | ------------------------------------------------------------------------ |
11
- | `.tooltip` | Wrapper around a trigger and a bubble. Paints nothing itself. |
12
- | `.tooltip-bubble` | The message. A real element, styled like any other surface. |
13
- | `.tooltip-icon` | Circular icon-style trigger, for a message with nothing else to hang on. |
14
- | `[data-tooltip-position="top"]` | Positions the bubble above the trigger, on `.tooltip`. |
15
- | `[data-tooltip-position="bottom"]` | Positions the bubble below the trigger, on `.tooltip`. |
16
- | `[data-tooltip-position="left"]` | Positions the bubble left of the trigger, on `.tooltip`. |
17
- | `[data-tooltip-position="right"]` | Positions the bubble right of the trigger, on `.tooltip`. |
18
- | No `data-tooltip-position` | Defaults to top placement. |
19
- | `[data-state="open"]` | Shows the bubble without hover, on `.tooltip`. |
20
- | `.tooltip-bubble.raised` / `.floating` | Adds one or two units of depth. The bubble is flat by default. |
21
-
22
- A tooltip is composition, not a single class: `.tooltip` wraps a trigger element and a `.tooltip-bubble`, and shows the bubble on hover or focus within the wrapper. This replaced a `::after` pseudo-element that carried the message as `content: attr(data-tooltip)` — CSS generated content is not reliably exposed to assistive tech, and cannot be the target of an `aria-describedby`, since that needs a real element with a real `id`. A real bubble can be. Wire the association yourself: give the bubble an `id`, give the trigger `aria-describedby` pointing at it, and put `role="tooltip"` on the bubble.
9
+ | Class or Attribute | Purpose |
10
+ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11
+ | `.tooltip` | Wrapper around a trigger and a bubble. Paints nothing itself. |
12
+ | `.tooltip-bubble` | The message. A real element, styled like any other surface. |
13
+ | `.tooltip-icon` | Round icon-style trigger, for a message with nothing else to hang on. It takes the aesthetic's corner: square under `.neobrutalism` and `.pixel`, a diamond under `.cyber`. |
14
+ | `[data-tooltip-position="top"]` | Positions the bubble above the trigger, on `.tooltip`. |
15
+ | `[data-tooltip-position="bottom"]` | Positions the bubble below the trigger, on `.tooltip`. |
16
+ | `[data-tooltip-position="left"]` | Positions the bubble left of the trigger, on `.tooltip`. |
17
+ | `[data-tooltip-position="right"]` | Positions the bubble right of the trigger, on `.tooltip`. |
18
+ | No `data-tooltip-position` | Defaults to top placement. |
19
+ | `[data-state="open"]` | Shows the bubble without hover, on `.tooltip`. |
20
+ | `[data-state="closed"]` | Hides the bubble even while hovered or focused, on `.tooltip`. For an Escape handler. |
21
+ | `.tooltip-bubble.raised` / `.floating` | Adds one or two units of depth. The bubble is flat by default. |
22
+
23
+ A tooltip is composition, not a single class: `.tooltip` wraps a trigger element and a `.tooltip-bubble`, and shows the bubble on hover or focus within the wrapper. The bubble is a real element because CSS generated content is not reliably exposed to assistive tech and cannot be the target of an `aria-describedby`, which needs a real element with a real `id`. Wire the association yourself: give the bubble an `id`, give the trigger `aria-describedby` pointing at it, and put `role="tooltip"` on the bubble.
23
24
 
24
25
  A tooltip bubble's fill, ground color, and floor behavior are covered in [Composing → Presentation](./composing.md#presentation), and its material follows whichever [aesthetic](./aesthetics.md) is in scope. Intent, presentation, aesthetic, and elevation classes go on `.tooltip-bubble` — the element that actually paints — not on `.tooltip`, the same rule every component in this package follows: put the class on the element that shows it.
25
26
 
26
- The bubble rests flat. It draws no shadow of its own on a plain page, and zeroes an aesthetic's part-based depth (such as `.neobrutalism`'s slab) the same way `.flat` does elsewhere. Add [`.raised` or `.floating`](./composing.md#elevation) to lift it, on the bubble itself — `.tooltip-bubble.floating` is the pre-0.1.0 look. An aesthetic whose shadow is a complete value, like `.glass`, still reaches the bubble as it reaches any surface.
27
+ The bubble rests flat. It draws no shadow of its own on a plain page, and zeroes an aesthetic's part-based depth (such as `.neobrutalism`'s slab) the same way `.flat` does elsewhere. Add [`.raised` or `.floating`](./composing.md#elevation) to lift it, on the bubble itself. An aesthetic whose shadow is a complete value, like `.glass`, still reaches the bubble as it reaches any surface.
27
28
 
28
29
  Intent works on a tooltip bubble the same way it works on a filled component: `.tooltip-bubble.primary` fills the bubble with the primary color, black text on a light page and white on a dark one; `.tooltip-bubble.destructive` is a red bubble with the same logic. With no intent, the bubble uses `--color-tooltip`, a plate chosen per theme rather than derived from an intent; see [Composing → Presentation](./composing.md#presentation) for why.
29
30
 
@@ -51,15 +52,34 @@ Intent works on a tooltip bubble the same way it works on a filled component: `.
51
52
  </span>
52
53
 
53
54
  <span class="tooltip" data-tooltip-position="right">
54
- <span class="tooltip-icon" tabindex="0" aria-label="More details" aria-describedby="icon-bubble">?</span>
55
+ <button type="button" class="tooltip-icon" aria-label="More details" aria-describedby="icon-bubble">?</button>
55
56
  <span class="tooltip-bubble" role="tooltip" id="icon-bubble">More details</span>
56
57
  </span>
57
58
  ```
58
59
 
59
60
  The first two show plain triggers with default (top) and explicit bottom placement. The third combines an intent with `.raised` on the bubble to show they compose freely — the intent decides the bubble's fill, `.raised` decides whether it casts depth. The fourth uses `data-state="open"` on the wrapper to keep a tooltip visible without a hover or focus interaction, useful for a tooltip that should be demonstrated or tested without simulating pointer input. The fifth uses `.tooltip-icon` as the trigger, for a message with no other element to hang it on.
60
61
 
61
- `.tooltip` composes with any trigger: wrap an existing `.btn` or `.badge` in it and both keep their own look, gaining a bubble beside them. `.tooltip-icon` is only the circular chip for the case where there is nothing else to be the trigger.
62
+ `.tooltip` composes with any trigger: wrap an existing `.btn` or `.badge` in it and both keep their own look, gaining a bubble beside them. `.tooltip-icon` is only the round chip for the case where there is nothing else to be the trigger.
62
63
 
63
64
  Showing and hiding the bubble is CSS-only — `:hover` and `:focus-within` on `.tooltip` — but that is presentational. It is not a complete accessible tooltip implementation by itself: provide the `id`/`aria-describedby`/`role="tooltip"` association outside the CSS; see [Accessibility](../accessibility.md).
64
65
 
66
+ The bubble is hoverable, as WCAG 1.4.13 asks of content that appears on hover: once the pointer leaves the trigger, the bubble stays for `--tooltip-linger` (`300ms`, set on `.tooltip`) and then fades, so the pointer can cross the gap onto it, and it stays open for as long as the pointer is over it. A closed bubble is `visibility: hidden`, so it is nothing under the pointer. It stays in the accessibility tree through `aria-describedby`, which reads hidden content, but a bubble with no `aria-describedby` pointing at it is no longer read in place while hidden.
67
+
68
+ 1.4.13 also asks that the message can be dismissed without moving the pointer or focus, which takes a key handler. Write `data-state="closed"` on `.tooltip` on Escape, and clear it when the pointer and focus leave:
69
+
70
+ ```js
71
+ for (const tooltip of document.querySelectorAll(".tooltip")) {
72
+ const reopen = () => tooltip.removeAttribute("data-state");
73
+ tooltip.addEventListener("pointerleave", reopen);
74
+ tooltip.addEventListener("focusout", reopen);
75
+ }
76
+
77
+ document.addEventListener("keydown", (event) => {
78
+ if (event.key !== "Escape") return;
79
+ for (const tooltip of document.querySelectorAll(".tooltip:hover, .tooltip:focus-within")) {
80
+ tooltip.dataset.state = "closed";
81
+ }
82
+ });
83
+ ```
84
+
65
85
  Next: [Content and layout](./content-and-layout.md) covers layout helpers, content chips and tables, surfaces, and typography utilities.