@uncinq/css-components 1.16.1 → 1.16.3

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 (50) hide show
  1. package/README.md +17 -15
  2. package/css/components/alert.css +5 -0
  3. package/css/components/panel.css +8 -6
  4. package/docs/_index.md +20 -16
  5. package/docs/buttons/_index.md +75 -0
  6. package/docs/buttons/btn-close.md +58 -0
  7. package/docs/buttons/btn-filter.md +36 -0
  8. package/docs/buttons/btn-menu.md +39 -0
  9. package/docs/buttons/btn-search.md +40 -0
  10. package/docs/buttons/btn-share.md +33 -0
  11. package/docs/buttons/btn-toc.md +23 -0
  12. package/docs/buttons/btn-toggle-video.md +58 -0
  13. package/docs/buttons/btn.md +95 -0
  14. package/docs/cascade-layers.md +9 -7
  15. package/docs/content/_index.md +39 -0
  16. package/docs/content/alert.md +97 -0
  17. package/docs/content/badge.md +50 -0
  18. package/docs/content/banner.md +43 -0
  19. package/docs/content/card.md +93 -0
  20. package/docs/content/items.md +76 -0
  21. package/docs/content/list.md +41 -0
  22. package/docs/content/media.md +88 -0
  23. package/docs/content/surtitle.md +43 -0
  24. package/docs/embeds/_index.md +25 -0
  25. package/docs/embeds/embed.md +53 -0
  26. package/docs/embeds/map.md +57 -0
  27. package/docs/embeds/video.md +53 -0
  28. package/docs/forms/_index.md +31 -0
  29. package/docs/forms/form-check.md +77 -0
  30. package/docs/forms/form.md +67 -0
  31. package/docs/navigation/_index.md +29 -0
  32. package/docs/navigation/breadcrumb.md +59 -0
  33. package/docs/navigation/nav-accessibility.md +67 -0
  34. package/docs/navigation/nav-title.md +43 -0
  35. package/docs/navigation/nav.md +65 -0
  36. package/docs/navigation/pagination.md +77 -0
  37. package/docs/overlays/_index.md +34 -0
  38. package/docs/overlays/drawer.md +58 -0
  39. package/docs/overlays/dropdown.md +87 -0
  40. package/docs/overlays/modal.md +75 -0
  41. package/docs/overlays/panel.md +159 -0
  42. package/docs/utilities/_index.md +21 -0
  43. package/docs/{utilities.md → utilities/scrollsnap.md} +26 -9
  44. package/package.json +1 -1
  45. package/docs/buttons.md +0 -103
  46. package/docs/content.md +0 -145
  47. package/docs/embeds.md +0 -66
  48. package/docs/forms.md +0 -80
  49. package/docs/navigation.md +0 -93
  50. package/docs/overlays.md +0 -133
package/README.md CHANGED
@@ -26,7 +26,7 @@ npm install @uncinq/design-tokens @uncinq/component-tokens @uncinq/css-base @unc
26
26
  Declare the cascade layer order once at the top of your entry stylesheet, **before any import**:
27
27
 
28
28
  ```css
29
- @layer reset, tokens, libs, vendors, base, layouts, components, pages, utilities;
29
+ @layer reset, tokens, libs, base, vendors, layouts, components, pages, utilities;
30
30
 
31
31
  @import '@uncinq/design-tokens'; /* @layer tokens */
32
32
  @import '@uncinq/css-base'; /* @layer reset, base, layouts */
@@ -41,11 +41,13 @@ Per component:
41
41
  @import '@uncinq/css-components/css/utilities/scrollsnap.css';
42
42
  ```
43
43
 
44
- Without a build step:
44
+ ### A CDN link is not enough
45
45
 
46
- ```html
47
- <link rel="stylesheet" href="https://unpkg.com/@uncinq/css-components">
48
- ```
46
+ `@uncinq/design-tokens` and `@uncinq/component-tokens` can be linked straight from a CDN, because they emit nothing but custom properties. **This package cannot**, and neither can `@uncinq/css-base`.
47
+
48
+ Both ship `@media (--sm)` queries that rely on the `@custom-media` rules declared in `css-base/css/mediaqueries.css`, and no browser implements `@custom-media`. Loaded without [postcss-custom-media](https://www.npmjs.com/package/postcss-custom-media), 13 responsive blocks in this package are dropped: `.panel-inline-*`, `.panel-trigger-*` and every `.scrollsnap-*` variant.
49
+
50
+ The failure is silent. No error is raised, the page simply stays at its mobile values on every screen. A build step is required.
49
51
 
50
52
  ## What's included
51
53
 
@@ -61,7 +63,7 @@ Without a build step:
61
63
 
62
64
  ## Two import-order rules
63
65
 
64
- `components/panel.css` is imported **last** among the components: its `.panel-inline-*` variants beat `.modal` and `.drawer` on source order, because most of the file is `:where()`-wrapped and carries no specificity.
66
+ `components/panel.css` must come **after** `modal.css` and `drawer.css`: its `.panel-inline-*` variants beat them on source order, because most of the file is `:where()`-wrapped and carries no specificity. Alphabetical order already gives that in `css/index.css`.
65
67
 
66
68
  `utilities/scrollsnap.css` comes after everything, in `@layer utilities`, because it has to win over the grid a component declares for itself.
67
69
 
@@ -71,22 +73,22 @@ If you import file by file rather than using the barrel, preserve both positions
71
73
 
72
74
  - **No JavaScript.** `modal`, `drawer` and `dropdown` expect a script; each documents the classes and attributes it must toggle.
73
75
  - **No `.item`.** This package ships `.card`, a complete implementation. `.item` lives in the Hugolify design system theme.
74
- - **No pagination glyphs.** The theme supplies them through `::before` or `::after` content.
76
+ - **No `.icon` glyphs.** Buttons and `pagination` mask their own from icon tokens, but an `.icon` element you place inside an `alert` or a video toggle is the theme's to render.
75
77
 
76
78
  ## Documentation
77
79
 
78
80
  Full documentation: **[socle.uncinq.dev/docs/css-components/](https://socle.uncinq.dev/docs/css-components/)**
79
81
 
80
- It is also versioned with the code in [`docs/`](docs/), and ships inside the npm package, so it is readable offline and from `node_modules`:
82
+ It is also versioned with the code in [`docs/`](docs/), one page per component, and ships inside the npm package, so it is readable offline and from `node_modules`:
81
83
 
82
84
  - [Cascade layers](docs/cascade-layers.md) — the two layers, and the `:where()` convention
83
- - [Buttons](docs/buttons.md)
84
- - [Overlays](docs/overlays.md) — the panel skeleton and the JS contracts
85
- - [Content](docs/content.md)
86
- - [Navigation](docs/navigation.md)
87
- - [Embeds](docs/embeds.md)
88
- - [Forms](docs/forms.md)
89
- - [Utilities](docs/utilities.md) — `.scrollsnap` and the bleed contract
85
+ - [Buttons](docs/buttons/) — [`.btn`](docs/buttons/btn.md) and the seven specialised buttons
86
+ - [Overlays](docs/overlays/) — [the panel skeleton](docs/overlays/panel.md), [`.modal`](docs/overlays/modal.md), [`.drawer`](docs/overlays/drawer.md), [`.dropdown`](docs/overlays/dropdown.md)
87
+ - [Content](docs/content/) — [`.alert`](docs/content/alert.md), [`.badge`](docs/content/badge.md), [`.banner`](docs/content/banner.md), [`.card`](docs/content/card.md), [`.items`](docs/content/items.md), [`.list`](docs/content/list.md), [`.media`](docs/content/media.md), [`.surtitle`](docs/content/surtitle.md)
88
+ - [Navigation](docs/navigation/) — [`.nav`](docs/navigation/nav.md), [`.nav-title`](docs/navigation/nav-title.md), [skip links](docs/navigation/nav-accessibility.md), [breadcrumb](docs/navigation/breadcrumb.md), [pagination](docs/navigation/pagination.md)
89
+ - [Embeds](docs/embeds/) — [`.embed`](docs/embeds/embed.md), [`.video`](docs/embeds/video.md), [`.map`](docs/embeds/map.md)
90
+ - [Forms](docs/forms/) — [`.form`](docs/forms/form.md), [`.form-check`](docs/forms/form-check.md)
91
+ - [Utilities](docs/utilities/) — [`.scrollsnap`](docs/utilities/scrollsnap.md) and the bleed contract
90
92
 
91
93
  ## References
92
94
 
@@ -38,6 +38,11 @@
38
38
  }
39
39
  }
40
40
 
41
+ /* Restore the hidden attribute, which display: flex would otherwise override */
42
+ .alert[hidden] {
43
+ display: none;
44
+ }
45
+
41
46
  /* Variants */
42
47
  .alert-brand {
43
48
  --alert-color-background: var(--color-brand-muted);
@@ -9,17 +9,19 @@
9
9
  * The tag picks the strategy, not the class. Either variant can be:
10
10
  * - <dialog> opened with showModal() — [open], native ::backdrop, top layer,
11
11
  * and the browser handles Escape, the focus trap and inertness;
12
- * - any element carrying popover="auto", opened by components/dialog.js
13
- * with showPopover() — same top layer, same ::backdrop, but no role of its
12
+ * - any element carrying popover="auto", opened with showPopover() by the
13
+ * script you provide — same top layer, same ::backdrop, but no role of its
14
14
  * own. Use this when the panel becomes a static region of the page above a
15
15
  * breakpoint (see .panel-inline-*): a <dialog> is exposed as role="dialog"
16
- * even when it is only styled as a sidebar, whereas dialog.js sets that
16
+ * even when it is only styled as a sidebar, whereas a script can set that
17
17
  * role solely while the panel is open.
18
18
  *
19
19
  * Most selectors here are :where()-wrapped so they carry no specificity: the
20
20
  * variants and the host components always win without escalating. The
21
21
  * .panel-inline-* variants are the exception — they must beat .modal and
22
- * .drawer, which they do on source order, so this file is imported last.
22
+ * .drawer, which they do on source order, so this file must be imported
23
+ * AFTER modal.css and drawer.css. Alphabetical order already gives that in
24
+ * css/index.css; keep it if you import file by file.
23
25
  *
24
26
  * Structure reads --panel-* tokens; each variant aliases them from its own
25
27
  * namespace, so --modal-* and --drawer-* stay the public API.
@@ -158,8 +160,8 @@
158
160
  * than patching every host component.
159
161
  *
160
162
  * The panel must NOT be a <dialog> when one of these is used — see the
161
- * strategy note at the top of this file. components/dialog.js also closes
162
- * an open panel carrying one of these classes when the viewport width
163
+ * strategy note at the top of this file. Your script must also close an
164
+ * open panel carrying one of these classes when the viewport width
163
165
  * changes, since CSS alone cannot release the backdrop, the scroll lock
164
166
  * and the inert background. */
165
167
 
package/docs/_index.md CHANGED
@@ -19,15 +19,17 @@ Every rule lives in `@layer components`, except the scroll-snap utility which li
19
19
 
20
20
  ## The components
21
21
 
22
- | Page | Covers |
22
+ One page per component, grouped into six sections.
23
+
24
+ | Section | Components |
23
25
  | --- | --- |
24
- | [Buttons](buttons/) | `.btn` with its colour, size and style variants, plus 7 specialised buttons |
25
- | [Overlays](overlays/) | `.panel`, `.modal`, `.drawer`, `.dropdown` |
26
- | [Content](content/) | `.alert`, `.badge`, `.banner`, `.card`, `.items`, `.list`, `.media`, `.surtitle` |
27
- | [Navigation](navigation/) | `.nav`, `.nav-title`, `.nav-accessibility`, `.breadcrumb-wrapper`, `.pagination` |
28
- | [Embeds](embeds/) | `.embed`, `.video`, `.map` |
29
- | [Forms](forms/) | `.form`, `.form-check` and the form layout helpers |
30
- | [Utilities](utilities/) | `.scrollsnap`, turning a grid into a snap carousel |
26
+ | [Buttons](buttons/) | [`.btn`](buttons/btn/), [`.btn-close`](buttons/btn-close/), [`.btn-menu`](buttons/btn-menu/), [`.btn-search`](buttons/btn-search/), [`.btn-filter`](buttons/btn-filter/), [`.btn-share`](buttons/btn-share/), [`.btn-toc`](buttons/btn-toc/), [`.btn-toggle-video`](buttons/btn-toggle-video/) |
27
+ | [Overlays](overlays/) | [panel](overlays/panel/), [`.modal`](overlays/modal/), [`.drawer`](overlays/drawer/), [`.dropdown`](overlays/dropdown/) |
28
+ | [Content](content/) | [`.alert`](content/alert/), [`.badge`](content/badge/), [`.banner`](content/banner/), [`.card`](content/card/), [`.items`](content/items/), [`.list`](content/list/), [`.media`](content/media/), [`.surtitle`](content/surtitle/) |
29
+ | [Navigation](navigation/) | [`.nav`](navigation/nav/), [`.nav-title`](navigation/nav-title/), [`.nav-accessibility`](navigation/nav-accessibility/), [`.breadcrumb-wrapper`](navigation/breadcrumb/), [`.pagination`](navigation/pagination/) |
30
+ | [Embeds](embeds/) | [`.embed`](embeds/embed/), [`.video`](embeds/video/), [`.map`](embeds/map/) |
31
+ | [Forms](forms/) | [`.form`](forms/form/), [`.form-check`](forms/form-check/) |
32
+ | [Utilities](utilities/) | [`.scrollsnap`](utilities/scrollsnap/) |
31
33
 
32
34
  Read [Cascade layers](cascade-layers/) first if you are wiring this into a project for the first time.
33
35
 
@@ -40,7 +42,7 @@ npm install @uncinq/design-tokens @uncinq/component-tokens @uncinq/css-base @unc
40
42
  ```
41
43
 
42
44
  ```css
43
- @layer reset, tokens, libs, vendors, base, layouts, components, pages, utilities;
45
+ @layer reset, tokens, libs, base, vendors, layouts, components, pages, utilities;
44
46
 
45
47
  @import '@uncinq/design-tokens'; /* @layer tokens */
46
48
  @import '@uncinq/css-base'; /* @layer reset, base, layouts */
@@ -56,11 +58,13 @@ Per component, when you only need a few:
56
58
  @import '@uncinq/css-components/css/utilities/scrollsnap.css';
57
59
  ```
58
60
 
59
- Without a build step:
61
+ ### A CDN link is not enough
60
62
 
61
- ```html
62
- <link rel="stylesheet" href="https://unpkg.com/@uncinq/css-components">
63
- ```
63
+ `@uncinq/design-tokens` and `@uncinq/component-tokens` can be linked straight from a CDN, because they emit nothing but custom properties. **This package cannot**, and neither can `@uncinq/css-base`.
64
+
65
+ Both ship `@media (--sm)` queries that rely on the `@custom-media` rules declared in `css-base/css/mediaqueries.css`, and no browser implements `@custom-media`. Loaded without [postcss-custom-media](https://www.npmjs.com/package/postcss-custom-media), 13 responsive blocks in this package are dropped: `.panel-inline-*`, `.panel-trigger-*` and every `.scrollsnap-*` variant.
66
+
67
+ The failure is silent. No error is raised, the page simply stays at its mobile values on every screen. A build step is required.
64
68
 
65
69
  Note that `@uncinq/css-base` is a genuine prerequisite but is **not** declared in `peerDependencies`. Install it explicitly.
66
70
 
@@ -68,11 +72,11 @@ Note that `@uncinq/css-base` is a genuine prerequisite but is **not** declared i
68
72
 
69
73
  `css/index.css` imports the components alphabetically, with two deliberate exceptions.
70
74
 
71
- `components/panel.css` is imported **last** among the components. Its `.panel-inline-*` variants have to beat `.modal` and `.drawer`, and they do so on source order rather than on specificity, because most of the file is wrapped in `:where()` and carries no specificity at all.
75
+ `components/button.css` comes **before** the seven `btn-*` files rather than after them, because five of those are modifiers on `.btn` and read better in that order.
72
76
 
73
77
  `utilities/scrollsnap.css` comes after everything, in `@layer utilities`, because it has to win over the grid a component declares for itself in `@layer components`.
74
78
 
75
- If you import file by file rather than using the barrel, preserve those two positions.
79
+ One constraint is easy to break when importing file by file: `panel.css` must come **after** `modal.css` and `drawer.css`. Alphabetical order already gives that, which is why nothing in the barrel looks unusual, but the `.panel-inline-*` variants beat the two host components on source order rather than on specificity. See [Cascade layers](cascade-layers/).
76
80
 
77
81
  ## What this package does not include
78
82
 
@@ -80,7 +84,7 @@ If you import file by file rather than using the barrel, preserve those two posi
80
84
 
81
85
  **No `.item`.** Despite what a couple of source comments still suggest, `.item` is not defined here. It lives in the Hugolify design system theme. This package ships `.card`, a complete implementation in its own right, and `.items`, a grid whose children you provide. See [Content](content/).
82
86
 
83
- **No icons.** Components that show a glyph, such as `.pagination` controls, expect the theme to supply it through `::before` or `::after` content.
87
+ **No icons for `.icon`.** Components that draw a glyph, the [buttons](buttons/) and [`.pagination`](navigation/pagination/), mask it from an icon token. An `.icon` element you place inside an [alert](content/alert/) or a [video toggle](buttons/btn-toggle-video/) is the theme's to render.
84
88
 
85
89
  ## References
86
90
 
@@ -0,0 +1,75 @@
1
+ ---
2
+ isIndex: false
3
+ title: Buttons
4
+ description: The .btn base class with its colour, size and style variants, and the seven specialised buttons built on it.
5
+ weight: 2
6
+ icon: hand-index
7
+ ---
8
+
9
+ Eight files: one base class, and seven single-purpose buttons.
10
+
11
+ | Page | Class | Needs `.btn` |
12
+ | --- | --- | --- |
13
+ | [Button](btn/) | `.btn` | — |
14
+ | [Close button](btn-close/) | `.btn-close` | no |
15
+ | [Menu button](btn-menu/) | `.btn-menu` | no |
16
+ | [Search button](btn-search/) | `.btn-search` | yes |
17
+ | [Filter button](btn-filter/) | `.btn-filter` | yes |
18
+ | [Share button](btn-share/) | `.btn-share` | yes |
19
+ | [Table of contents button](btn-toc/) | `.btn-toc` | yes |
20
+ | [Video toggle](btn-toggle-video/) | `.btn-toggle-video` | yes |
21
+
22
+ ## Two kinds of specialised button
23
+
24
+ Read the third column before using one, because the two kinds behave differently when the base class is missing.
25
+
26
+ `.btn-close` and `.btn-menu` **redeclare everything they need**. Each `--btn-close-*` and `--btn-menu-*` token falls back to the matching `--btn-*` one, so they look consistent with `.btn` without depending on it. Use them on their own.
27
+
28
+ The other five are **modifiers**. They declare nothing but `--btn-color-*` reassignments and an icon, so omitting `.btn` leaves them almost entirely unstyled.
29
+
30
+ ```html
31
+ <!-- self-contained -->
32
+ <button class="btn-close" aria-label="Close"></button>
33
+
34
+ <!-- modifier on .btn -->
35
+ <button class="btn btn-search" aria-label="Search"></button>
36
+ ```
37
+
38
+ ## The icons are masks, not images
39
+
40
+ Six of the seven ship their glyph, drawn on `::before` from the icon tokens in [@uncinq/design-tokens](../../design-tokens/reference/#icon):
41
+
42
+ ```css
43
+ &::before {
44
+ background-color: currentColor;
45
+ mask: var(--icon-search) center / var(--btn-search-icon-size, var(--icon-size)) no-repeat;
46
+ }
47
+ ```
48
+
49
+ Because it is a mask over a background colour rather than a background image, the glyph takes `currentColor` and follows the button's text colour through every state, including hover and disabled. Size it globally with `--icon-size`, or per button with `--btn-close-icon-size` and its siblings.
50
+
51
+ `.btn` publishes `--icon-size: var(--btn-font-size)`, so a modifier button scales with its size class for free. A bare `.btn-close` or `.btn-menu` has no such declaration above it and takes whatever `--icon-size` is in scope.
52
+
53
+ [`.btn-toggle-video`](btn-toggle-video/) is the exception: it ships no icon and expects two child elements instead.
54
+
55
+ ## None of them names itself
56
+
57
+ When a button has no visible text, give it an accessible name with `aria-label`, or a `.visually-hidden` span from [css-base](../../css-base/base/#accessibility-helpers). No button in this package provides one.
58
+
59
+ ## Focus and disabled states
60
+
61
+ `.btn` declares both:
62
+
63
+ ```css
64
+ &:focus-visible {
65
+ outline: var(--focus-outline-width) var(--focus-outline-style) var(--focus-color-outline);
66
+ }
67
+
68
+ &:disabled,
69
+ &[aria-disabled='true'] {
70
+ cursor: not-allowed;
71
+ opacity: var(--opacity-disabled);
72
+ }
73
+ ```
74
+
75
+ The five modifiers inherit that through the base class. **`.btn-close` and `.btn-menu` declare neither**, so they fall back to the browser's own focus ring rather than the design system's, and a disabled one looks no different from an enabled one. Add `.btn` alongside them when either matters.
@@ -0,0 +1,58 @@
1
+ ---
2
+ isIndex: false
3
+ title: Close button
4
+ description: The self-contained close control used inside panels, alerts and dismissible blocks.
5
+ weight: 2
6
+ icon: x-lg
7
+ ---
8
+
9
+ A close control. It is **self-contained**: `.btn` is not required.
10
+
11
+ ```html
12
+ <button class="btn-close" aria-label="Close"></button>
13
+ ```
14
+
15
+ Its main use is the header of a [panel](../../overlays/panel/), where it usually carries the `.js-dialog-close` hook as well:
16
+
17
+ ```html
18
+ <div class="header">
19
+ <p class="title" id="contactLabel">Contact</p>
20
+ <button class="btn-close js-dialog-close" aria-label="Close"></button>
21
+ </div>
22
+ ```
23
+
24
+ ## Why it stands alone
25
+
26
+ Every property is declared twice over, with `--btn-close-*` falling back to the matching `--btn-*` token:
27
+
28
+ ```css
29
+ background-color: var(--btn-close-color-background, var(--btn-control-color-background));
30
+ border-radius: var(--btn-close-border-radius, var(--btn-border-radius));
31
+ font-size: var(--btn-close-font-size, var(--btn-font-size));
32
+ ```
33
+
34
+ So it looks like the rest of the button family without inheriting from it, and a single `--btn-close-*` override retargets it alone. Its default palette is `--btn-control-*`, the same quiet colours as [`.btn-control`](../btn/#style-variants), which is what keeps it from competing with the panel's real actions.
35
+
36
+ ## The icon
37
+
38
+ Drawn on `::before` as a mask from `--icon-close`, sized by `--btn-close-icon-size` falling back to `--icon-size`. Since nothing above a bare `.btn-close` publishes `--icon-size`, set one explicitly if the ambient value is not what you want:
39
+
40
+ ```css
41
+ .btn-close {
42
+ --btn-close-icon-size: var(--font-size-lg);
43
+ }
44
+ ```
45
+
46
+ ## What it does not declare
47
+
48
+ No `:focus-visible` rule and no disabled styling. The focus ring is therefore the browser's own rather than the one from the `--focus-*` tokens, and `disabled` changes nothing visually.
49
+
50
+ Add `.btn` alongside it when either matters:
51
+
52
+ ```html
53
+ <button class="btn btn-close" aria-label="Close"></button>
54
+ ```
55
+
56
+ ## Accessibility
57
+
58
+ The button has no text content, so `aria-label` is not optional. A close button with no accessible name is announced as "button" and nothing more.
@@ -0,0 +1,36 @@
1
+ ---
2
+ isIndex: false
3
+ title: Filter button
4
+ description: The filters toggle, a modifier on .btn carrying the filter icon.
5
+ weight: 5
6
+ icon: funnel
7
+ ---
8
+
9
+ The control that opens a filters panel. **It requires `.btn`.**
10
+
11
+ ```html
12
+ <button class="btn btn-filter panel-trigger-md js-dialog-toggle"
13
+ data-target="#filters"
14
+ aria-label="Filters"></button>
15
+ ```
16
+
17
+ Like [`.btn-search`](../btn-search/), it declares only a `--btn-color-*` set pointed at the control palette, with `--btn-filter-color-*` in front of it as the override hook, and an icon masked from `--icon-filter`. Everything else comes from `.btn`.
18
+
19
+ ## It usually needs a `.panel-trigger-*`
20
+
21
+ A filters panel is the textbook case for [`.panel-inline-*`](../../overlays/panel/#inline-variants): a drawer on mobile, a sidebar column on desktop. Once the panel is a column there is nothing left to open, so the button has to go with it.
22
+
23
+ The two classes are separate and must be paired by hand:
24
+
25
+ | Panel carries | Button carries |
26
+ | --- | --- |
27
+ | `.panel-inline-sm` | `.panel-trigger-sm` |
28
+ | `.panel-inline-md` | `.panel-trigger-md` |
29
+ | `.panel-inline-lg` | `.panel-trigger-lg` |
30
+ | `.panel-inline-xl` | `.panel-trigger-xl` |
31
+
32
+ A mismatch leaves a button on screen that opens nothing. The reasoning behind the two-class design is on the [panel page](../../overlays/panel/#hiding-the-trigger).
33
+
34
+ ## Placement
35
+
36
+ The filter button usually lives in the page heading rather than beside the panel it opens, which is worth remembering when writing layout rules around it. It is an ordinary inline-flex button and takes whatever the surrounding layout gives it.
@@ -0,0 +1,39 @@
1
+ ---
2
+ isIndex: false
3
+ title: Menu button
4
+ description: The self-contained menu toggle, typically the burger in a site header.
5
+ weight: 3
6
+ icon: list
7
+ ---
8
+
9
+ A menu toggle, typically the burger control in a header. Like [`.btn-close`](../btn-close/), it is **self-contained**: `.btn` is not required.
10
+
11
+ ```html
12
+ <button class="btn-menu js-dialog-toggle" data-target="#menu" aria-label="Menu"></button>
13
+ ```
14
+
15
+ It is almost always the trigger for a [drawer](../../overlays/drawer/), which is why it usually carries `.js-dialog-toggle` and a `data-target` pointing at the panel's `id`.
16
+
17
+ ## Same construction as the close button
18
+
19
+ Every property falls back from `--btn-menu-*` to the matching `--btn-*` token, and the default palette is `--btn-control-*`. See [`.btn-close`](../btn-close/#why-it-stands-alone) for the full explanation, which applies here unchanged.
20
+
21
+ The icon comes from `--icon-menu`, masked on `::before` and sized by `--btn-menu-icon-size` falling back to `--icon-size`.
22
+
23
+ ## What it does not declare
24
+
25
+ No `:focus-visible` rule and no disabled styling, exactly as with `.btn-close`. Add `.btn` alongside it when you need either.
26
+
27
+ ## Hiding it once the menu is inline
28
+
29
+ A burger that opens a panel which has become a static region of the page is a button that does nothing. Pair the panel's `.panel-inline-*` class with the matching `.panel-trigger-*` on this button:
30
+
31
+ ```html
32
+ <button class="btn-menu panel-trigger-md js-dialog-toggle" data-target="#menu" aria-label="Menu"></button>
33
+ ```
34
+
35
+ See [Hiding the trigger](../../overlays/panel/#hiding-the-trigger).
36
+
37
+ ## Accessibility
38
+
39
+ `aria-label` is required, since the button has no text. Keep `aria-expanded` in sync if your script toggles a menu in place rather than opening a panel in the top layer.
@@ -0,0 +1,40 @@
1
+ ---
2
+ isIndex: false
3
+ title: Search button
4
+ description: The search toggle, a modifier on .btn carrying the search icon.
5
+ weight: 4
6
+ icon: search
7
+ ---
8
+
9
+ A search control. **It requires `.btn`**: on its own it is nothing but a colour set and an icon.
10
+
11
+ ```html
12
+ <button class="btn btn-search js-dialog-toggle" data-target="#search" aria-label="Search"></button>
13
+ ```
14
+
15
+ ## What it declares
16
+
17
+ Two things, and nothing else.
18
+
19
+ The `--btn-color-*` set is retargeted at the control palette, with a per-button escape hatch in front of it:
20
+
21
+ ```css
22
+ --btn-color-background: var(--btn-search-color-background, var(--btn-control-color-background));
23
+ --btn-color-text: var(--btn-search-color-text, var(--btn-control-color-text));
24
+ ```
25
+
26
+ Override `--btn-search-color-*` to restyle every search button, or `--btn-color-*` directly on one instance.
27
+
28
+ And the glyph, masked from `--icon-search` on `::before`, sized by `--btn-search-icon-size` falling back to `--icon-size`, which `.btn` sets to its own font size.
29
+
30
+ That is the whole file. Geometry, focus, hover, disabled and the transition all come from the base class, which is why omitting it leaves an unstyled button with an icon.
31
+
32
+ ## Adding a label
33
+
34
+ The icon sits on `::before`, so any text content lands beside it with `--btn-gap` between the two. No extra class is needed:
35
+
36
+ ```html
37
+ <button class="btn btn-search">Search</button>
38
+ ```
39
+
40
+ Without visible text, `aria-label` is required.
@@ -0,0 +1,33 @@
1
+ ---
2
+ isIndex: false
3
+ title: Share button
4
+ description: The share control, a modifier on .btn carrying the share icon.
5
+ weight: 6
6
+ icon: share
7
+ ---
8
+
9
+ A share control. **It requires `.btn`.**
10
+
11
+ ```html
12
+ <button class="btn btn-share" aria-label="Share">Share</button>
13
+ ```
14
+
15
+ Construction is identical to [`.btn-search`](../btn-search/): a `--btn-color-*` set pointed at the control palette, with `--btn-share-color-*` as the per-variant override, and the glyph masked from `--icon-share`.
16
+
17
+ ## It ships no behaviour
18
+
19
+ The class is presentation only. Whether the button calls `navigator.share()`, copies a URL to the clipboard or opens a [dropdown](../../overlays/dropdown/) of destinations is entirely up to your script.
20
+
21
+ If it opens a menu, the dropdown contract applies: the trigger needs `.dropdown-toggle` and `.js-dropdown-toggle`, and its next element sibling must be the `.dropdown-menu`.
22
+
23
+ ```html
24
+ <div class="dropdown">
25
+ <button class="btn btn-share dropdown-toggle js-dropdown-toggle" aria-expanded="false">Share</button>
26
+ <ul class="dropdown-menu">
27
+ <li><a href="...">Bluesky</a></li>
28
+ <li><a href="...">LinkedIn</a></li>
29
+ </ul>
30
+ </div>
31
+ ```
32
+
33
+ Note that `.dropdown-toggle` draws its own chevron on `::after` while `.btn-share` draws the share glyph on `::before`, so the combination gives you both, one on each side of the label.
@@ -0,0 +1,23 @@
1
+ ---
2
+ isIndex: false
3
+ title: Table of contents button
4
+ description: The table of contents toggle, a modifier on .btn carrying the toc icon.
5
+ weight: 7
6
+ icon: list-nested
7
+ ---
8
+
9
+ The control that opens a table of contents. **It requires `.btn`.**
10
+
11
+ ```html
12
+ <button class="btn btn-toc panel-trigger-lg js-dialog-toggle"
13
+ data-target="#toc"
14
+ aria-label="Table of contents"></button>
15
+ ```
16
+
17
+ Construction is identical to [`.btn-search`](../btn-search/): a `--btn-color-*` set pointed at the control palette, with `--btn-toc-color-*` as the per-variant override, and the glyph masked from `--icon-toc`.
18
+
19
+ ## It behaves like the filter button
20
+
21
+ A table of contents follows the same responsive pattern as a filters panel: a drawer on small screens, a sticky column on large ones. Pair the panel's [`.panel-inline-*`](../../overlays/panel/#inline-variants) with the matching `.panel-trigger-*` on this button, or you keep a button that opens nothing at desktop width.
22
+
23
+ Because a table of contents is typically a [`.nav`](../../navigation/nav/) inside the panel's `.content`, the panel's colour handover matters here: going inline hands `--nav-color-text` back to the page, so the links stop using the panel's text colour and take the site's link colour instead.
@@ -0,0 +1,58 @@
1
+ ---
2
+ isIndex: false
3
+ title: Video toggle
4
+ description: The play and pause control overlaid on an autoplaying video, driven by the .is-playing state class.
5
+ weight: 8
6
+ icon: play-circle
7
+ ---
8
+
9
+ A play and pause control overlaid on a video. **It requires `.btn`**, and it is the one specialised button that ships no icon of its own.
10
+
11
+ ```html
12
+ <div class="video">
13
+ <video autoplay muted loop playsinline></video>
14
+ <button class="btn btn-toggle-video is-playing" aria-label="Pause">
15
+ <span class="icon-pause"></span>
16
+ <span class="icon-play"></span>
17
+ </button>
18
+ </div>
19
+ ```
20
+
21
+ ## Why it exists
22
+
23
+ A video that autoplays without native controls must be pausable to meet WCAG. This button is that pause control. If your video carries `controls`, you do not need it.
24
+
25
+ It stays fully transparent, background and border alike, so the video shows through:
26
+
27
+ ```css
28
+ --btn-color-background: transparent;
29
+ --btn-color-border: transparent;
30
+ ```
31
+
32
+ The [`.video`](../../embeds/video/) wrapper is what positions it, pinned to the bottom inline-end corner. Used outside that wrapper it is an ordinary transparent button in the flow.
33
+
34
+ ## Two children, one visible
35
+
36
+ The whole file beyond the colours is this:
37
+
38
+ ```css
39
+ &:not(.is-playing) .icon-pause { display: none; }
40
+ &.is-playing .icon-play { display: none; }
41
+ ```
42
+
43
+ So the button expects **two** child elements and shows exactly one. The glyphs themselves are not provided here: `.icon-pause` and `.icon-play` are whatever your theme's icon system renders.
44
+
45
+ ## What your script owns
46
+
47
+ Two things, and getting either wrong breaks the control.
48
+
49
+ **Toggling `.is-playing`** in step with the video's actual state, including when playback stops for a reason the script did not initiate.
50
+
51
+ **Keeping the accessible name in sync.** The control means *pause* while playing and *play* while paused, so a fixed `aria-label` is wrong half the time.
52
+
53
+ ```js
54
+ button.classList.toggle('is-playing', !video.paused);
55
+ button.setAttribute('aria-label', video.paused ? 'Play' : 'Pause');
56
+ ```
57
+
58
+ Note that `.is-playing` describes the **video**, not the button, which is why the class stays on while the video runs rather than flipping on click.