@uncinq/css-components 1.16.2 → 1.16.4

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 (58) hide show
  1. package/README.md +11 -10
  2. package/css/components/alert.css +5 -0
  3. package/css/components/panel.css +8 -6
  4. package/docs/_index.md +0 -84
  5. package/docs/buttons/_index.md +7 -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/buttons/overview.md +75 -0
  15. package/docs/cascade-layers.md +9 -7
  16. package/docs/content/_index.md +7 -0
  17. package/docs/content/alert.md +97 -0
  18. package/docs/content/badge.md +50 -0
  19. package/docs/content/banner.md +43 -0
  20. package/docs/content/card.md +93 -0
  21. package/docs/content/items.md +76 -0
  22. package/docs/content/list.md +41 -0
  23. package/docs/content/media.md +88 -0
  24. package/docs/content/overview.md +39 -0
  25. package/docs/content/surtitle.md +43 -0
  26. package/docs/embeds/_index.md +7 -0
  27. package/docs/embeds/embed.md +53 -0
  28. package/docs/embeds/map.md +57 -0
  29. package/docs/embeds/overview.md +25 -0
  30. package/docs/embeds/video.md +53 -0
  31. package/docs/forms/_index.md +7 -0
  32. package/docs/forms/form-check.md +77 -0
  33. package/docs/forms/form.md +67 -0
  34. package/docs/forms/overview.md +31 -0
  35. package/docs/navigation/_index.md +7 -0
  36. package/docs/navigation/breadcrumb.md +59 -0
  37. package/docs/navigation/nav-accessibility.md +67 -0
  38. package/docs/navigation/nav-title.md +43 -0
  39. package/docs/navigation/nav.md +65 -0
  40. package/docs/navigation/overview.md +29 -0
  41. package/docs/navigation/pagination.md +77 -0
  42. package/docs/overlays/_index.md +7 -0
  43. package/docs/overlays/drawer.md +58 -0
  44. package/docs/overlays/dropdown.md +87 -0
  45. package/docs/overlays/modal.md +75 -0
  46. package/docs/overlays/overview.md +34 -0
  47. package/docs/overlays/panel.md +159 -0
  48. package/docs/overview.md +93 -0
  49. package/docs/utilities/_index.md +7 -0
  50. package/docs/utilities/overview.md +21 -0
  51. package/docs/{utilities.md → utilities/scrollsnap.md} +26 -9
  52. package/package.json +1 -1
  53. package/docs/buttons.md +0 -103
  54. package/docs/content.md +0 -145
  55. package/docs/embeds.md +0 -66
  56. package/docs/forms.md +0 -80
  57. package/docs/navigation.md +0 -93
  58. package/docs/overlays.md +0 -133
package/README.md CHANGED
@@ -63,7 +63,7 @@ The failure is silent. No error is raised, the page simply stays at its mobile v
63
63
 
64
64
  ## Two import-order rules
65
65
 
66
- `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`.
67
67
 
68
68
  `utilities/scrollsnap.css` comes after everything, in `@layer utilities`, because it has to win over the grid a component declares for itself.
69
69
 
@@ -73,22 +73,23 @@ If you import file by file rather than using the barrel, preserve both positions
73
73
 
74
74
  - **No JavaScript.** `modal`, `drawer` and `dropdown` expect a script; each documents the classes and attributes it must toggle.
75
75
  - **No `.item`.** This package ships `.card`, a complete implementation. `.item` lives in the Hugolify design system theme.
76
- - **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.
77
77
 
78
78
  ## Documentation
79
79
 
80
80
  Full documentation: **[socle.uncinq.dev/docs/css-components/](https://socle.uncinq.dev/docs/css-components/)**
81
81
 
82
- 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`:
83
83
 
84
+ - [Overview](docs/overview.md) — installation, import order, what the package leaves out
84
85
  - [Cascade layers](docs/cascade-layers.md) — the two layers, and the `:where()` convention
85
- - [Buttons](docs/buttons.md)
86
- - [Overlays](docs/overlays.md) — the panel skeleton and the JS contracts
87
- - [Content](docs/content.md)
88
- - [Navigation](docs/navigation.md)
89
- - [Embeds](docs/embeds.md)
90
- - [Forms](docs/forms.md)
91
- - [Utilities](docs/utilities.md) — `.scrollsnap` and the bleed contract
86
+ - [Buttons](docs/buttons/) — [`.btn`](docs/buttons/btn.md) and the seven specialised buttons
87
+ - [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)
88
+ - [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)
89
+ - [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)
90
+ - [Embeds](docs/embeds/) — [`.embed`](docs/embeds/embed.md), [`.video`](docs/embeds/video.md), [`.map`](docs/embeds/map.md)
91
+ - [Forms](docs/forms/) — [`.form`](docs/forms/form.md), [`.form-check`](docs/forms/form-check.md)
92
+ - [Utilities](docs/utilities/) — [`.scrollsnap`](docs/utilities/scrollsnap.md) and the bleed contract
92
93
 
93
94
  ## References
94
95
 
@@ -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
@@ -5,87 +5,3 @@ description: Framework-agnostic CSS component implementations, driven entirely b
5
5
  weight: 5
6
6
  icon: grid
7
7
  ---
8
-
9
- `@uncinq/css-components` is the top of the stack: 29 components and 1 utility, written as plain CSS, reading their values from [@uncinq/component-tokens](../component-tokens/) and adding no values of their own.
10
-
11
- ```
12
- @uncinq/design-tokens primitive + semantic values
13
- @uncinq/component-tokens component-scoped values
14
- @uncinq/css-base reset, native elements, layouts
15
- @uncinq/css-components ← this package
16
- ```
17
-
18
- Every rule lives in `@layer components`, except the scroll-snap utility which lives in `@layer utilities`. There is no build step, no preprocessor, and no JavaScript shipped in the package. Three components do expect a script you provide, and each one documents the contract it needs.
19
-
20
- ## The components
21
-
22
- | Page | Covers |
23
- | --- | --- |
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 |
31
-
32
- Read [Cascade layers](cascade-layers/) first if you are wiring this into a project for the first time.
33
-
34
- ## Installation
35
-
36
- All three packages below are required. The tokens resolve the custom properties, and css-base provides the reset and the element styles the components build on.
37
-
38
- ```bash
39
- npm install @uncinq/design-tokens @uncinq/component-tokens @uncinq/css-base @uncinq/css-components
40
- ```
41
-
42
- ```css
43
- @layer reset, tokens, libs, base, vendors, layouts, components, pages, utilities;
44
-
45
- @import '@uncinq/design-tokens'; /* @layer tokens */
46
- @import '@uncinq/css-base'; /* @layer reset, base, layouts */
47
- @import '@uncinq/component-tokens'; /* @layer tokens */
48
- @import '@uncinq/css-components'; /* @layer components, utilities */
49
- ```
50
-
51
- Per component, when you only need a few:
52
-
53
- ```css
54
- @import '@uncinq/css-components/css/components/button.css';
55
- @import '@uncinq/css-components/css/components/alert.css';
56
- @import '@uncinq/css-components/css/utilities/scrollsnap.css';
57
- ```
58
-
59
- ### A CDN link is not enough
60
-
61
- `@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`.
62
-
63
- 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.
64
-
65
- The failure is silent. No error is raised, the page simply stays at its mobile values on every screen. A build step is required.
66
-
67
- Note that `@uncinq/css-base` is a genuine prerequisite but is **not** declared in `peerDependencies`. Install it explicitly.
68
-
69
- ## Import order inside the package
70
-
71
- `css/index.css` imports the components alphabetically, with two deliberate exceptions.
72
-
73
- `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.
74
-
75
- `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`.
76
-
77
- If you import file by file rather than using the barrel, preserve those two positions.
78
-
79
- ## What this package does not include
80
-
81
- **No JavaScript.** `.modal`, `.drawer` and `.dropdown` expect a script. The CSS defines the classes and attributes that script must toggle, and each page documents the contract.
82
-
83
- **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/).
84
-
85
- **No icons.** Components that show a glyph, such as `.pagination` controls, expect the theme to supply it through `::before` or `::after` content.
86
-
87
- ## References
88
-
89
- - [@uncinq/component-tokens](https://github.com/uncinq/component-tokens), the values these components read
90
- - [@uncinq/css-base](https://github.com/uncinq/css-base), the foundation underneath
91
- - [MDN: CSS cascade layers](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Cascade_layers)
@@ -0,0 +1,7 @@
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: 3
6
+ icon: hand-index
7
+ ---
@@ -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: 3
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: 6
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: 4
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: 5
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: 7
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: 8
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: 9
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.
@@ -0,0 +1,95 @@
1
+ ---
2
+ isIndex: false
3
+ title: Button
4
+ description: The .btn base class, its ten colour variants, five sizes and three style variants.
5
+ weight: 2
6
+ icon: hand-index-thumb
7
+ ---
8
+
9
+ `.btn` is the base class. It works on `<button>`, `<a>` and `<input type="submit">`, and reads `--btn-*` from [@uncinq/component-tokens](../../../component-tokens/reference/).
10
+
11
+ ```html
12
+ <button class="btn">Send</button>
13
+ <a class="btn btn-secondary" href="/contact">Contact</a>
14
+ ```
15
+
16
+ Variants compose freely: `class="btn btn-danger btn-sm btn-ghost"`.
17
+
18
+ ## Colour variants
19
+
20
+ | Class | Intent |
21
+ | --- | --- |
22
+ | `.btn-primary` | Primary action |
23
+ | `.btn-secondary` | Secondary action |
24
+ | `.btn-brand` | Brand-coloured |
25
+ | `.btn-neutral` | Neutral emphasis |
26
+ | `.btn-dark` | Dark surface |
27
+ | `.btn-light` | Light surface |
28
+ | `.btn-success` | Confirmation |
29
+ | `.btn-danger` | Destructive action |
30
+ | `.btn-warning` | Warning |
31
+ | `.btn-info` | Informational |
32
+
33
+ Each sets four properties together: background, border, their hover values, and the text colour.
34
+
35
+ ```css
36
+ .btn-danger {
37
+ --btn-color-background: var(--color-danger);
38
+ --btn-color-background-hover: var(--color-danger-strong);
39
+ --btn-color-border: var(--color-danger);
40
+ --btn-color-border-hover: var(--color-danger-strong);
41
+ --btn-color-text: var(--color-text-on-danger);
42
+ }
43
+ ```
44
+
45
+ The text colour always comes from the matching `--color-text-on-*` semantic token, so contrast survives a change to the underlying colour. That pairing is the reason to rebrand by redefining `--color-brand` rather than `--btn-color-background`.
46
+
47
+ `.btn-primary` and `.btn-brand` resolve to exactly the same values, as do `.btn-secondary` and `.btn-neutral`. The intent names exist so a page can say *primary action* without deciding which colour that is.
48
+
49
+ ## Sizes
50
+
51
+ | Class | Use |
52
+ | --- | --- |
53
+ | `.btn-xs` | Dense UI, inline controls |
54
+ | `.btn-sm` | Compact |
55
+ | `.btn-md` | Default, can be omitted |
56
+ | `.btn-lg` | Prominent |
57
+ | `.btn-xl` | Hero call to action |
58
+
59
+ A size class sets `--btn-font-size` along with the block and inline padding, all three from the same rung of the scale. Since `.btn` publishes `--icon-size: var(--btn-font-size)`, any icon inside scales with it.
60
+
61
+ ## Style variants
62
+
63
+ | Class | Effect |
64
+ | --- | --- |
65
+ | `.btn-ghost` | Fully transparent, background and border alike, text in `--color-text` |
66
+ | `.btn-link` | Renders as an underlined link |
67
+ | `.btn-control` | Form control styling, for buttons sitting next to inputs |
68
+
69
+ `.btn-control` maps the `--btn-color-*` set onto the `--btn-control-*` tokens, which is the same palette the [close](../btn-close/) and [menu](../btn-menu/) buttons use by default. It is what makes a button read as part of a form rather than as a call to action.
70
+
71
+ ### How `.btn-link` gets its underline
72
+
73
+ Every button already carries one. The base class declares `text-decoration-line: var(--btn-text-decoration-line)`, which the tokens set to `underline`, and paints it with `--btn-color-text-decoration`, which defaults to `transparent`.
74
+
75
+ `.btn-link` only changes the colour:
76
+
77
+ ```css
78
+ .btn-link {
79
+ --btn-color-text-decoration: initial; /* revealed, takes currentColor */
80
+
81
+ @media (hover: hover) and (pointer: fine) {
82
+ &:hover {
83
+ --btn-color-text-decoration: transparent; /* hidden again */
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ The underline is therefore always in the layout and never shifts the text, which is why hovering one does not nudge the label. If you write your own link-like variant, change the decoration colour rather than the decoration line, for the same reason.
90
+
91
+ ## States
92
+
93
+ `:focus-visible` draws the outline from the `--focus-*` tokens. `:disabled` and `[aria-disabled='true']` are treated alike, both getting `cursor: not-allowed` and `--opacity-disabled`.
94
+
95
+ Hover is guarded by `@media (hover: hover) and (pointer: fine)`, so a touch device never leaves a button stuck in its hover colours after a tap. The transition is guarded by `prefers-reduced-motion`.
@@ -0,0 +1,75 @@
1
+ ---
2
+ isIndex: false
3
+ title: Overview
4
+ description: The eight button files, the two kinds of specialised button, and the masked icons they share.
5
+ weight: 1
6
+ icon: book
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/). 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.