actual-css 0.13.0 → 0.14.1

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 (66) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +8 -8
  3. package/components.json +116 -10
  4. package/dist/actual.full.css +652 -77
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/docs/design-notes/widget-primitives.md +4 -0
  8. package/docs/navigation.json +2 -0
  9. package/docs/pages/components/alert.md +12 -2
  10. package/docs/pages/components/button.md +3 -2
  11. package/docs/pages/components/list.md +6 -3
  12. package/docs/pages/components/segmented.md +83 -0
  13. package/docs/pages/components/steps.md +4 -2
  14. package/docs/pages/enhancements/flyout.md +9 -3
  15. package/docs/pages/enhancements/overview.md +2 -1
  16. package/docs/pages/enhancements/tabs.md +7 -2
  17. package/docs/pages/enhancements/tooltip.md +3 -1
  18. package/docs/pages/examples/overview.md +5 -3
  19. package/docs/pages/forms/color-swatches.md +131 -0
  20. package/docs/pages/forms/combobox.md +2 -2
  21. package/docs/pages/forms/controls.md +6 -3
  22. package/docs/pages/forms/joined-controls.md +6 -2
  23. package/docs/pages/forms/overview.md +1 -0
  24. package/docs/pages/forms/switch.md +4 -2
  25. package/docs/pages/foundations/theming.md +1 -1
  26. package/docs/pages/foundations/tokens.md +54 -23
  27. package/docs/pages/guides/design-tools.md +13 -7
  28. package/docs/pages/guides/introduction.md +1 -1
  29. package/docs/pages/layout/scroll-snap.md +4 -2
  30. package/package.json +5 -4
  31. package/reserved-classes.json +3 -0
  32. package/src/cli/actual-css.js +3 -3
  33. package/src/css/components/alert.css +65 -14
  34. package/src/css/components/app-nav.css +17 -9
  35. package/src/css/components/badge.css +23 -9
  36. package/src/css/components/button.css +17 -2
  37. package/src/css/components/close.css +6 -3
  38. package/src/css/components/index.css +1 -0
  39. package/src/css/components/list.css +18 -3
  40. package/src/css/components/navbar.css +5 -2
  41. package/src/css/components/segmented.css +186 -0
  42. package/src/css/components/steps.css +6 -3
  43. package/src/css/components/tab.css +1 -1
  44. package/src/css/core/focus.css +3 -0
  45. package/src/css/core/tokens.css +20 -4
  46. package/src/css/core/variants.css +16 -5
  47. package/src/css/forms/color-swatch.css +163 -0
  48. package/src/css/forms/form-actions.css +9 -0
  49. package/src/css/forms/index.css +1 -0
  50. package/src/css/forms/native.css +50 -20
  51. package/src/css/forms/switch.css +5 -2
  52. package/src/css/layout/scroll-snap.css +30 -0
  53. package/src/css/layout/topbar.css +10 -0
  54. package/src/js/enhance.js +14 -0
  55. package/src/js/floating-space.js +26 -0
  56. package/src/js/flyout.js +3 -1
  57. package/src/js/focus.js +16 -1
  58. package/src/js/parse-config.js +5 -2
  59. package/src/js/surface.js +10 -0
  60. package/src/js/tooltip.js +2 -2
  61. package/src/tooling/color.js +14 -1
  62. package/src/tooling/css-bundle.js +0 -5
  63. package/src/tooling/design-components.js +9 -5
  64. package/src/tooling/design-export.js +8 -0
  65. package/src/tooling/design-tokens.js +163 -43
  66. package/src/tooling/theme-contrast.js +151 -22
@@ -35,6 +35,10 @@ The named exports each module carries:
35
35
  - `selectors` — `CLASSES`
36
36
 
37
37
  `surface` also owns outside-click and Escape dismissal, and focus restore.
38
+ A click on an element with `data-flyout-close` closes the open panel around
39
+ it, whichever widget opened that panel. The attribute keeps the `flyout` name
40
+ because the panels authors write are flyouts; `surface` is only the code that
41
+ opens and closes them.
38
42
  `menu` filters to usable items.
39
43
 
40
44
  ## Focus group
@@ -69,6 +69,7 @@
69
69
  "pagination",
70
70
  "progress",
71
71
  "rating",
72
+ "segmented",
72
73
  "shimmer",
73
74
  "skeleton",
74
75
  "spinner",
@@ -91,6 +92,7 @@
91
92
  "form-actions",
92
93
  "joined-controls",
93
94
  "choice-cards",
95
+ "color-swatches",
94
96
  "switch",
95
97
  "range",
96
98
  "otp",
@@ -12,7 +12,7 @@
12
12
  - Could have simple or complex html content.
13
13
  - A title and a description are plain siblings: the alert owns the small space between its direct content blocks, so neither needs a wrapper. Nested content keeps its own rhythm — wrap several paragraphs in `.prose`, or a differently spaced group in `.stack`, when the message needs a rhythm of its own.
14
14
  - Alerts may include a decorative leading icon. Use `.alert-icon` on the icon element; the rest of the content flows into the remaining text column.
15
- - Use `.compact` or `.spacious` for density changes. The inline padding stays stable.
15
+ - Use `.compact` or `.spacious` on the alert to step its block padding. The inline padding stays stable.
16
16
  - Use a [`.close`](close.md) button to make the alert dismissible. It is a direct trailing child in standard alerts and lives inside `.alert-title` in admonitions; the alert makes it compact.
17
17
 
18
18
  Because alerts are soft by default, adding `.soft` to an intent (`.alert.soft.primary`,
@@ -216,7 +216,7 @@ full-bleed notice.
216
216
 
217
217
  ## Admonition
218
218
 
219
- An admonition is a structured box with a tinted title bar, an optional icon, and body content on the page surface — like the `!!! note` callouts in mkdocs. Use `.alert-title` for the header and `.alert-body` for the content. Intent classes tint the title bar background and the border.
219
+ An admonition is a structured box with a tinted title bar, an optional icon, and body content on the page surface — like the `!!! note` callouts in mkdocs. Use `.alert-title` for the header and `.alert-body` for the content. Intent classes tint the title bar background and the border; shared variants paint the title bar, so `.solid` fills it — and keeps the body's frame neutral — for a notification that needs to stand out.
220
220
 
221
221
  ```html demo
222
222
  <div class="stack">
@@ -239,6 +239,16 @@ An admonition is a structured box with a tinted title bar, an optional icon, and
239
239
  <p>The intent tints the title bar, the border, and the body uses <code>var(--surface)</code>.</p>
240
240
  </div>
241
241
  </div>
242
+
243
+ <div class="alert success admonition solid">
244
+ <div class="alert-title">
245
+ <i class="ti ti-circle-check" aria-hidden="true"></i>
246
+ Saved
247
+ </div>
248
+ <div class="alert-body">
249
+ <p>A solid title bar for an action response.</p>
250
+ </div>
251
+ </div>
242
252
  </div>
243
253
  ```
244
254
 
@@ -153,8 +153,9 @@ Keep one variant family inside a group. Because each button keeps its own
153
153
  classes, nothing stops `.btn`, `.btn outline` and `.btn soft` from sitting side
154
154
  by side — but three fills and three border treatments in one shape read as
155
155
  unrelated controls that happen to touch, not as one unit. Change the variant
156
- only to carry meaning: [Toggle](#toggle) marks the active segment with a solid
157
- button among outlines, which is the pattern a segmented control wants.
156
+ only to carry meaning: [Toggle](#toggle) marks a pressed button with a solid
157
+ fill among outlines. For one choice among a few on a shared track, use a
158
+ [segmented control](segmented.md) instead.
158
159
 
159
160
  ## Sizes
160
161
 
@@ -29,9 +29,12 @@ list markers when order or bullets carry meaning.
29
29
  ```
30
30
 
31
31
  Leading and trailing content are unconstrained: avatars, native choices,
32
- badges, text, switches, and buttons retain their own component sizing.
33
- `.list-item-content` has no minimum content floor, so supporting text and long
34
- tokens wrap instead of widening the page.
32
+ badges, text, switches, and buttons retain their own component sizing. In a
33
+ narrow row, the trailing region moves under the content rather than breaking a
34
+ word of the title; supporting text still wraps beside it, and a token longer
35
+ than the row breaks instead of widening the page. A `.truncate` line inside
36
+ `.list-item-content` gives up its width, so it ellipsizes next to the trailing
37
+ region (wrap, responsive row, overflow).
35
38
 
36
39
  Both outer regions are optional, and an absent one costs neither space nor a
37
40
  gap: `.list-item-content` always takes the row's free space and
@@ -0,0 +1,83 @@
1
+ # Segmented control
2
+
3
+ > One compact choice among a few options, on a shared track with the selected segment raised.
4
+
5
+ **Related terms:** segmented control, toggle group, pill tabs, view switcher, button group.
6
+
7
+ `.segmented` is the track and each `.segment` a direct child. The look is
8
+ shared; the semantics come from the markup, so pick the anatomy by what the
9
+ choice does:
10
+
11
+ | Use | Markup | Selected | Keyboard |
12
+ | ------------- | ---------------------- | ---------------------- | ------------- |
13
+ | Pick a value | `<fieldset>` of radios | `:checked` | Native arrows |
14
+ | Switch panels | `role="tablist"` | `aria-selected="true"` | Tabs runtime |
15
+ | Navigate | `<nav>` of links | `aria-current="page"` | Tab, Enter |
16
+
17
+ Segments share the width equally and the track is at least `--control-size`
18
+ tall, so it lines up with `.btn` and `.input`; `.sm` and `.lg` size it like
19
+ any control. A long label wraps instead of truncating, and the row grows with
20
+ it. A parent that stretches its children (`.stack`) makes it full width; add
21
+ `.align-self-start` to keep it at its content width.
22
+
23
+ ## Pick a value
24
+
25
+ Wrap each radio in its label. The radio is visually hidden but stays focusable
26
+ and submitted; in a browser without `:has()` it stays visible beside the
27
+ label.
28
+
29
+ ```html demo
30
+ <fieldset class="segmented">
31
+ <legend class="sr-only">Range</legend>
32
+ <label class="segment"><input type="radio" name="range" value="day">Day</label>
33
+ <label class="segment"><input type="radio" name="range" value="week" checked>Week</label>
34
+ <label class="segment"><input type="radio" name="range" value="month">Month</label>
35
+ </fieldset>
36
+ ```
37
+
38
+ ## Switch panels
39
+
40
+ A tablist with `data-enhance="tabs"` takes the [tabs](../enhancements/tabs.md)
41
+ runtime unchanged: it reads the roles, not the classes.
42
+
43
+ ```html demo
44
+ <div class="segmented" role="tablist" aria-label="View" data-enhance="tabs">
45
+ <button class="segment" type="button" role="tab" id="view-list-tab" aria-selected="true" aria-controls="view-list">List</button>
46
+ <button class="segment" type="button" role="tab" id="view-grid-tab" aria-selected="false" aria-controls="view-grid" tabindex="-1">Grid</button>
47
+ </div>
48
+ <section role="tabpanel" id="view-list" aria-labelledby="view-list-tab">List view</section>
49
+ <section role="tabpanel" id="view-grid" aria-labelledby="view-grid-tab" hidden>Grid view</section>
50
+ ```
51
+
52
+ ## Navigate
53
+
54
+ ```html demo
55
+ <nav class="segmented" aria-label="Period">
56
+ <a class="segment" href="#day" aria-current="page">Day</a>
57
+ <a class="segment" href="#week">Week</a>
58
+ <a class="segment" href="#month">Month</a>
59
+ </nav>
60
+ ```
61
+
62
+ ## When to use something else
63
+
64
+ - Several options on at once (bold, italic): `.join` of `.btn` with
65
+ `aria-pressed`, see [joined controls](../forms/joined-controls.md).
66
+ - More options than fit on one row: [tabs](../enhancements/tabs.md), whose
67
+ strip scrolls.
68
+ - Long or descriptive options: [choice cards](../forms/choice-cards.md) or
69
+ plain radios.
70
+
71
+ ## Hooks
72
+
73
+ - `--segmented-bg` — track fill.
74
+ - `--segmented-pad` — inset between the track and its segments, also the gap
75
+ between segments; the segment radius is the track radius minus it.
76
+ - `--segmented-radius` — track corner radius.
77
+ - `--segmented-fg` — label ink of the unselected segments; the selected one
78
+ takes `--text`.
79
+ - `--segmented-selected-bg` — fill of the selected segment. Its shadow follows
80
+ `--shadow`, so a flat theme removes it at the theme level.
81
+ - `--segmented-selected-border` — hairline around the selected segment. It is
82
+ what sets the segment apart where its fill is darker than the track; a theme
83
+ whose fill is lighter can fade it.
@@ -80,9 +80,11 @@ accent, override the tokens on the component:
80
80
  }
81
81
  ```
82
82
 
83
- `--state-selected` drives the completed fill, connector and the current ring;
83
+ `--state-selected` drives the completed fill and connector;
84
84
  `--state-selected-fg` corrects the number inside a completed marker when the
85
- accent needs a lighter ink.
85
+ accent needs a lighter ink. The current marker sits on the surface, so its
86
+ number and ring read `--state-selected-text` first
87
+ ([tokens](../foundations/tokens.md)).
86
88
 
87
89
  ## Horizontal steps
88
90
 
@@ -67,9 +67,10 @@ For a modal bottom sheet or action sheet, use `dialog.drawer`; use
67
67
  `dialog.modal` for a centered modal. Those components own modal focus,
68
68
  dismissal, scroll locking, and backdrop behavior explicitly.
69
69
 
70
- Use `data-flyout-placement` for the preferred anchored placement. It accepts the
71
- placement strings supported by the floating runtime, such as `bottom-start`,
72
- `bottom-end`, `top-start`, `right`, or `left`.
70
+ Use `data-flyout-placement` for the preferred anchored placement: a side
71
+ (`top`, `bottom`, `left`, `right`), optionally followed by `-start` or `-end`,
72
+ such as `bottom-start` or `right`. An unknown value logs a console warning and
73
+ falls back to `bottom-start`.
73
74
 
74
75
  Use `data-flyout-distance` for the trigger gap in pixels. The default is `4`.
75
76
 
@@ -180,6 +181,11 @@ flyout when that specific control must close it regardless of the automatic
180
181
  policy. This is useful for an Apply or Done action in an `outside` or `false`
181
182
  panel. The trigger and Escape continue to close the flyout in every mode.
182
183
 
184
+ Arrow keys and Space in a radio group never close the flyout: they move
185
+ through the options, and a pointer pick closes it. Opening from the keyboard
186
+ focuses a group at its checked radio. The [palette picker](../forms/color-swatches.md#palette-in-a-flyout)
187
+ relies on both.
188
+
183
189
  ```html
184
190
  <div class="flyout"
185
191
  id="filters"
@@ -56,7 +56,8 @@ is a no-op until the module is loaded again with a DOM.
56
56
  Load the full bundle **or** the source modules, never both: each evaluation
57
57
  owns its registry and document handlers, so a dist bundle mixed with
58
58
  `actual-css/js/*` source subpaths (or two dist bundles) registers every
59
- behavior twice and a flyout opens then immediately closes.
59
+ behavior twice and a flyout opens then immediately closes. The second copy
60
+ logs a console warning when it loads.
60
61
 
61
62
  ## Component pages
62
63
 
@@ -91,10 +91,15 @@ JavaScript involved:
91
91
 
92
92
  ## Equal-width tabs
93
93
 
94
- To make a few tabs share the strip's width (segmented, full-width, fill), set
94
+ To make a few tabs share the strip's width (full-width, fill), set
95
95
  `--tab-flex: 1 1 0` on the strip. `.switcher` does not apply here: a tab strip
96
96
  never wraps, so it could never stack.
97
97
 
98
+ For a few tabs on a track with a raised selected tab (segmented control, pill
99
+ tabs), put the same tablist on [`.segmented`](../components/segmented.md):
100
+ the runtime is unchanged. Its labels wrap instead of scrolling, so keep it to
101
+ what fits on one row.
102
+
98
103
  ```html demo
99
104
  <nav aria-label="Pricing sections">
100
105
  <ul class="tabs" style="--tab-flex: 1 1 0">
@@ -158,5 +163,5 @@ Do not put a flyout trigger in a `role="tablist"`: a tablist holds only
158
163
  ## CSS hooks
159
164
 
160
165
  - `--tab-flex` — flex of each item; set `1 1 0` on the strip for equal-width tabs
161
- (segmented, fill the width). A strip that cannot fit its labels still scrolls.
166
+ (fill the width). A strip that cannot fit its labels still scrolls.
162
167
  - `--tab-gap` — space between an icon and the label text (default `0.375em`).
@@ -115,7 +115,9 @@ viewport-aware maximum width.
115
115
  ## Positioning
116
116
 
117
117
  Set `data-tooltip-placement` to control where the tooltip appears relative to
118
- its trigger. The arrow follows the placement automatically.
118
+ its trigger: a side (`top`, `bottom`, `left`, `right`), optionally followed by
119
+ `-start` or `-end`. An unknown value logs a console warning and falls back to
120
+ `top`. The arrow follows the placement automatically.
119
121
 
120
122
  The runtime also picks the coordinate space, and writes `position` to match. A
121
123
  tooltip whose trigger scrolls with the page is placed in document coordinates
@@ -4,8 +4,8 @@
4
4
 
5
5
  The examples live in the repository under `demo/` and load `actual.full.css`
6
6
  and `actual.full.js` from `dist/`, plus the theme palettes bundle from
7
- `demo/assets/`. The bundle is committed and regenerated by
8
- `bun run build:themes`, so the pages open from a fresh checkout.
7
+ `demo/assets/`. Both are build output: in a clone, run `bun run build:dist`,
8
+ `bun run build:js` and `bun run build:themes` before opening a page.
9
9
 
10
10
  ## Templates
11
11
 
@@ -23,13 +23,15 @@ rest show it composed, isolated, or under stress.
23
23
  Whole pages, composed the way a real product would.
24
24
 
25
25
  - [Dashboard](../../demo/templates/dashboard.html) - data-heavy overview page
26
+ - [Order management](../../demo/templates/order-management.html) - one state at three levels (a badge in a selectable list, a message in context, a notification with its own action) composed on the `admin-neutral` business-interface theme
26
27
  - [App](../../demo/templates/app.html) - application shell with sidebar
27
28
  - [Workspaces](../../demo/templates/workspaces.html) - sectioned sidebar, master/detail list with native current state, and workspace cards
28
29
  - [Marketing](../../demo/templates/marketing.html) - landing page with typography and fluid type
29
30
  - [Blog](../../demo/templates/blog.html) - article page with prose
30
31
  - [Settings modal](../../demo/templates/settings-modal.html) - two-pane preferences dialog, with the local CSS it needs audited rule by rule
31
32
  - [Modal patterns](../../demo/templates/modal-patterns.html) - eight application modals, each the modal surface plus the smallest application CSS it needs
32
- - [Workspace settings](../../demo/templates/settings-pricing.html) - plans with visible native choices, preferences and a local segmented tab treatment
33
+ - [Workspace settings](../../demo/templates/settings-pricing.html) - plans with visible native choices, preferences and segmented tabs
34
+ - [iOS-style settings](../../demo/templates/settings-ios.html) - iOS-like controls and grouped settings from unchanged framework markup, a local theme, and its findings listed rule by rule
33
35
  - [Card compositions](../../demo/templates/card-compositions.html) - editorial, team, sign-in and horizontal cards composed from existing primitives
34
36
  - [Register form](../../demo/templates/register-form.html) - registration page with choice cards, masks, the password toggle, `tel-prefix` validation, an OTP verification dialog, and status feedback
35
37
 
@@ -0,0 +1,131 @@
1
+ # Color swatches
2
+
3
+ > Pick a color from a palette the application defines, inline or from a flyout.
4
+
5
+ **Related terms:** color picker, palette, swatch, color chip, accent color, label color.
6
+
7
+ `.color-swatch` is a `<label>` wrapping a native radio, with the color in
8
+ `--color-swatch-value`. The radio stays the control: it submits, takes the
9
+ arrow keys and carries the name in `aria-label`. The checked swatch shows a
10
+ double ring that reads on any color. Use checkboxes for several colors at once.
11
+
12
+ For a free color, use the native [`.color`](controls.md) picker; a custom
13
+ spectrum or RGB sliders are outside Actual.
14
+
15
+ ## Palette
16
+
17
+ Lay the swatches out with `.cluster` or `.grid`, and space them with `--gap`
18
+ (`.tight`, `.loose` or an inline value), not `gap`: the hit area reaches
19
+ half of `--gap` past the swatch, up to `--control-size`, so neighbours split
20
+ the gap between them. A tighter palette trades target size for density. Set
21
+ the column count with `--grid-columns: repeat(6, max-content)` on `.grid`.
22
+
23
+ ```html demo
24
+ <fieldset class="field-group cluster">
25
+ <legend>Category color</legend>
26
+ <label class="color-swatch" style="--color-swatch-value: #2563eb"><input type="radio" name="category" value="#2563eb" aria-label="Blue" checked></label>
27
+ <label class="color-swatch" style="--color-swatch-value: #10b981"><input type="radio" name="category" value="#10b981" aria-label="Green"></label>
28
+ <label class="color-swatch" style="--color-swatch-value: #f59e0b"><input type="radio" name="category" value="#f59e0b" aria-label="Amber"></label>
29
+ <label class="color-swatch" style="--color-swatch-value: #e11d48"><input type="radio" name="category" value="#e11d48" aria-label="Rose"></label>
30
+ <label class="color-swatch" style="--color-swatch-value: #ffffff"><input type="radio" name="category" value="#ffffff" aria-label="White"></label>
31
+ </fieldset>
32
+ ```
33
+
34
+ ## Palette in a flyout
35
+
36
+ A `<button class="color-swatch">` shows the current color and opens the
37
+ palette as a [flyout](../enhancements/flyout.md). A pointer pick closes it;
38
+ the arrow keys move through the colors with the panel open, and Escape returns
39
+ to the button. Give the panel `--flyout-pad` so the focus ring fits inside it.
40
+ Application code shows the pick on the button.
41
+
42
+ ```html demo
43
+ <span class="field-label" id="accent-label">Accent color</span>
44
+ <div class="flyout-trigger">
45
+ <button class="color-swatch" type="button" data-enhance="flyout"
46
+ aria-controls="accent-palette" aria-expanded="false"
47
+ aria-labelledby="accent-label accent-name"
48
+ style="--color-swatch-value: #2563eb">
49
+ <span class="sr-only" id="accent-name">Blue</span>
50
+ </button>
51
+ <div class="flyout" id="accent-palette" style="--flyout-pad: var(--space-30)" hidden>
52
+ <div class="cluster" role="radiogroup" aria-labelledby="accent-label">
53
+ <label class="color-swatch" style="--color-swatch-value: #2563eb"><input type="radio" name="accent" value="#2563eb" aria-label="Blue" checked></label>
54
+ <label class="color-swatch" style="--color-swatch-value: #10b981"><input type="radio" name="accent" value="#10b981" aria-label="Green"></label>
55
+ <label class="color-swatch" style="--color-swatch-value: #f59e0b"><input type="radio" name="accent" value="#f59e0b" aria-label="Amber"></label>
56
+ <label class="color-swatch" style="--color-swatch-value: #e11d48"><input type="radio" name="accent" value="#e11d48" aria-label="Rose"></label>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ <script>
61
+ (() => {
62
+ const preview = document.currentScript.closest(".docs-preview");
63
+ const trigger = preview.querySelector("button.color-swatch");
64
+ preview.querySelector(".flyout").addEventListener("change", (event) => {
65
+ trigger.style.setProperty("--color-swatch-value", event.target.value);
66
+ trigger.querySelector(".sr-only").textContent = event.target.getAttribute("aria-label");
67
+ });
68
+ })();
69
+ </script>
70
+ ```
71
+
72
+ ## Palette and custom color
73
+
74
+ Add the native [`.color`](controls.md) picker to the panel for a color
75
+ outside the palette. Two changes to the flyout above:
76
+
77
+ - `data-flyout-auto-close="outside"` on the panel, so opening the color dialog
78
+ does not close it, and `data-flyout-close` on each swatch, so a pointer pick
79
+ still does. The arrow keys still move through the palette with the panel
80
+ open.
81
+ - The custom input sits in a `<label class="cluster">`, which centers it on
82
+ its text; `.choice` aligns checkboxes, radios and switches only.
83
+ - The custom input takes its own name. Application code unchecks the palette
84
+ when a custom color is chosen, so the form sends `brand`, or `brand-custom`
85
+ when no palette color is checked.
86
+
87
+ ```html demo
88
+ <span class="field-label" id="brand-label">Brand color</span>
89
+ <div class="flyout-trigger">
90
+ <button class="color-swatch" type="button" data-enhance="flyout"
91
+ aria-controls="brand-palette" aria-expanded="false"
92
+ aria-labelledby="brand-label brand-name"
93
+ style="--color-swatch-value: #2563eb">
94
+ <span class="sr-only" id="brand-name">Blue</span>
95
+ </button>
96
+ <div class="flyout stack" id="brand-palette" data-flyout-auto-close="outside"
97
+ style="--flyout-pad: var(--space-30)" hidden>
98
+ <div class="cluster" role="radiogroup" aria-labelledby="brand-label">
99
+ <label class="color-swatch" data-flyout-close style="--color-swatch-value: #2563eb"><input type="radio" name="brand" value="#2563eb" aria-label="Blue" checked></label>
100
+ <label class="color-swatch" data-flyout-close style="--color-swatch-value: #10b981"><input type="radio" name="brand" value="#10b981" aria-label="Green"></label>
101
+ <label class="color-swatch" data-flyout-close style="--color-swatch-value: #f59e0b"><input type="radio" name="brand" value="#f59e0b" aria-label="Amber"></label>
102
+ <label class="color-swatch" data-flyout-close style="--color-swatch-value: #e11d48"><input type="radio" name="brand" value="#e11d48" aria-label="Rose"></label>
103
+ </div>
104
+ <label class="cluster"><input class="color" type="color" name="brand-custom" value="#7048e8"> Custom…</label>
105
+ </div>
106
+ </div>
107
+ <script>
108
+ (() => {
109
+ const preview = document.currentScript.closest(".docs-preview");
110
+ const trigger = preview.querySelector("button.color-swatch");
111
+ const palette = preview.querySelector(".flyout");
112
+ palette.addEventListener("change", (event) => {
113
+ const custom = event.target.type === "color";
114
+ if (custom) for (const radio of palette.querySelectorAll('[type="radio"]')) radio.checked = false;
115
+ trigger.style.setProperty("--color-swatch-value", event.target.value);
116
+ trigger.querySelector(".sr-only").textContent = custom
117
+ ? `Custom ${event.target.value}`
118
+ : event.target.getAttribute("aria-label");
119
+ });
120
+ })();
121
+ </script>
122
+ ```
123
+
124
+ The color dialog is the browser's own and looks different on each platform.
125
+
126
+ ## Hooks
127
+
128
+ - `--color-swatch-value` — the color, set on each swatch.
129
+ - `--color-swatch-size` — visible swatch size, the height of a `.switch` track
130
+ by default. `.sm` and `.lg` scale it and the hit area.
131
+ - `--color-swatch-radius` — corner radius; round by default.
@@ -93,9 +93,9 @@ contrast contracts apply to the combobox for free:
93
93
 
94
94
  .actual-combobox .cb-chip {
95
95
  border: var(--border-width) solid
96
- color-mix(in oklab, var(--surface) var(--soft-border-mix), var(--primary));
96
+ color-mix(in oklab, var(--primary) calc(100% - var(--soft-border-mix, 100%)), transparent);
97
97
  border-radius: var(--radius-full);
98
- background: color-mix(in oklab, var(--surface) var(--soft-bg-mix), var(--primary));
98
+ background: color-mix(in oklab, var(--primary) calc(100% - var(--soft-bg-mix)), transparent);
99
99
  color: color-mix(in oklab, var(--primary) var(--soft-fg-mix), var(--text));
100
100
  font-size: var(--font-size-xs);
101
101
  font-weight: var(--font-weight-semibold);
@@ -6,9 +6,12 @@ Native-only control chrome: the file input and color picker mostly defeat custom
6
6
 
7
7
  > Small, theme-aware chrome for the native color picker.
8
8
 
9
- Use `.color` on an `input[type="color"]` to give the native picker a control-sized
10
- box with the framework's border, radius, surface, disabled, and focus styles.
11
- The color dialog and selected-color swatch remain browser-native.
9
+ Use `.color` on an `input[type="color"]` to show the native picker as a round
10
+ swatch the height of a [switch](switch.md), on a control-sized hit area, with
11
+ the framework's edge, disabled, and focus styles. `--color-size` sets the
12
+ swatch. The color dialog remains browser-native. To choose from a fixed
13
+ palette instead, use [color swatches](color-swatches.md), which share this
14
+ geometry.
12
15
 
13
16
  ```html demo
14
17
  <label class="field">
@@ -2,7 +2,11 @@
2
2
 
3
3
  > Use `.join` when controls need to be visually joined into a single unit.
4
4
 
5
- **Related terms:** segmented control, button group, input group.
5
+ **Related terms:** button group, input group, toolbar.
6
+
7
+ `.join` groups controls that keep their own identity and function. For one
8
+ choice among a few on a shared track, use a
9
+ [segmented control](../components/segmented.md).
6
10
 
7
11
  ## Class reference
8
12
 
@@ -13,7 +17,7 @@
13
17
 
14
18
  Add `.join` to the wrapper that groups the controls. Use `.join-addon` for static prefix/suffix content such as currency symbols, units, or protocol text. Action buttons inside the group should use `.btn`. The wrapper should still carry `role="group"` with a label for accessibility.
15
19
 
16
- `.join` reshapes corners and shared edges; it does not unify the segments' treatments, and the children keep whatever classes they carry. Give them one variant family so the group reads as a single unit, and change a segment's variant only to mark state — a solid button among outlines for the active segment of a segmented control. A group mixing `.btn`, `.btn outline` and `.btn soft` is valid markup that renders as three unrelated controls stuck together.
20
+ `.join` reshapes corners and shared edges; it does not unify the segments' treatments, and the children keep whatever classes they carry. Give them one variant family so the group reads as a single unit, and change a segment's variant only to mark state — a solid button among outlines for a pressed toggle. A group mixing `.btn`, `.btn outline` and `.btn soft` is valid markup that renders as three unrelated controls stuck together.
17
21
 
18
22
  When composing source files manually, import `components/join.css` after the controls it groups. `.join` writes child border-radius longhands directly so joined corners win at equal specificity.
19
23
 
@@ -28,6 +28,7 @@
28
28
  | `.range` | Component | Native range slider, theme-aware accent. |
29
29
  | `.file` | Component | Native file input, themed picker button. |
30
30
  | `.color` | Component | Native color picker in a control-sized box. |
31
+ | `.color-swatch` | Component | Palette color on a radio, or its trigger. |
31
32
  | `.field` | Composition | Field wrapper, on `<label>` or `<div>`. |
32
33
  | `.field-label` | Composition | Label text, element-agnostic. |
33
34
  | `.field-help` | Composition | Supporting helper text under a control. |
@@ -132,8 +132,10 @@ same optical line. Change the type or the control size and they stay level:
132
132
  - `--switch-width` — track width.
133
133
  - `--switch-block-size` — track height.
134
134
  - `--switch-knob-margin` — inset between knob and track.
135
+ - `--switch-knob-inline-size` — knob width; wider than its height makes a
136
+ capsule knob. Defaults to the knob height.
135
137
 
136
- Knob size and travel distance are derived from the three `--switch-*` hooks; set
137
- those rather than the derived values. The knob is concentric in its track: the
138
+ Knob size and travel distance are derived from these hooks; set them rather
139
+ than the derived values. The knob is concentric in its track: the
138
140
  inset is measured inside the border on both axes, so it clears the rails by the
139
141
  same distance it clears the ends.
@@ -56,7 +56,7 @@ Set `--form-actions-justify` (and `--form-actions-align`).
56
56
 
57
57
  ### Customize a switch's size
58
58
 
59
- Change `--switch-width`, `--switch-block-size`, and `--switch-knob-margin`; knob size and travel are derived from them. To rescale the whole row of inline choices at once, use `.sm` / `.lg` on `.choice`, or set `--choice-control-size` directly — the checkbox, the radio, and the switch track all follow it. Density contexts leave inline choices alone.
59
+ Change `--switch-width`, `--switch-block-size`, and `--switch-knob-margin`; knob size and travel are derived from them. `--switch-knob-inline-size` widens the knob into a capsule. To rescale the whole row of inline choices at once, use `.sm` / `.lg` on `.choice`, or set `--choice-control-size` directly — the checkbox, the radio, and the switch track all follow it. Density contexts leave inline choices alone.
60
60
 
61
61
  → Forms · Switch
62
62