@uncinq/css-components 1.15.3 → 1.16.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.
package/README.md CHANGED
@@ -1,134 +1,99 @@
1
1
  # @uncinq/css-components
2
2
 
3
- > Framework-agnostic CSS component implementations — actual styles built on top of design and component tokens.
3
+ > Framework-agnostic CSS component implementations.
4
4
 
5
5
  <img width="1280" height="640" alt="share-css-components" src="https://github.com/user-attachments/assets/3d74df8a-e7bf-4ba5-8529-99cfef0bc176" />
6
6
 
7
- ## What is this?
7
+ 29 components and 1 utility, written as plain CSS, reading their values from [@uncinq/component-tokens](https://github.com/uncinq/component-tokens) and adding none of their own.
8
8
 
9
- `@uncinq/css-components` provides the CSS implementations of common UI components. It sits at the top of the CSS stack:
9
+ ## Where this sits
10
10
 
11
- ```text
12
- @uncinq/design-tokens ← primitive + semantic CSS custom properties
13
- @uncinq/component-tokens ← component-scoped CSS custom properties
14
- @uncinq/css-base ← base CSS rules
15
- @uncinq/css-components ← actual CSS rules (this package)
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
16
  ```
17
17
 
18
- Each component file contains only CSS rules, using the tokens defined in the two packages above. No JavaScript, no markup opinions — just portable, layered CSS.
18
+ ## Installation
19
19
 
20
- ---
20
+ All four packages are required. `@uncinq/css-base` is a genuine prerequisite but is **not** declared in `peerDependencies`, so install it explicitly.
21
21
 
22
- ## CSS cascade layers
22
+ ```bash
23
+ npm install @uncinq/design-tokens @uncinq/component-tokens @uncinq/css-base @uncinq/css-components
24
+ ```
23
25
 
24
- Component styles are declared inside `@layer components`, utilities inside `@layer utilities`:
26
+ Declare the cascade layer order once at the top of your entry stylesheet, **before any import**:
25
27
 
26
28
  ```css
27
29
  @layer reset, tokens, libs, vendors, base, layouts, components, pages, utilities;
28
- ```
29
-
30
- This means:
31
-
32
- - Components never bleed into base or layout styles
33
- - Project-level `@layer components` rules always win over these defaults
34
- - Third-party stylesheets (`libs`, e.g. Splide) sit at the bottom, and the CSS
35
- that overrides them (`vendors`) right above — so a lazily loaded library
36
- cannot win on source order
37
- - Utilities come last, so `.scrollsnap` wins over the grid a component declares for itself
38
30
 
39
- ---
40
-
41
- ## Prerequisites
31
+ @import '@uncinq/design-tokens'; /* @layer tokens */
32
+ @import '@uncinq/css-base'; /* @layer reset, base, layouts */
33
+ @import '@uncinq/component-tokens'; /* @layer tokens */
34
+ @import '@uncinq/css-components'; /* @layer components, utilities */
35
+ ```
42
36
 
43
- This package consumes tokens from:
37
+ Per component:
44
38
 
45
- - [@uncinq/design-tokens](https://github.com/uncinq/design-tokens) — semantic CSS custom properties
46
- - [@uncinq/component-tokens](https://github.com/uncinq/component-tokens) — component-scoped CSS custom properties
47
- - [@uncinq/css-base](https://github.com/uncinq/css-base) — base CSS rules
39
+ ```css
40
+ @import '@uncinq/css-components/css/components/button.css';
41
+ @import '@uncinq/css-components/css/utilities/scrollsnap.css';
42
+ ```
48
43
 
49
- Import them before this package:
44
+ Without a build step:
50
45
 
51
- ```css
52
- @import '@uncinq/design-tokens';
53
- @import '@uncinq/component-tokens';
54
- @import '@uncinq/css-base';
55
- @import '@uncinq/css-components';
46
+ ```html
47
+ <link rel="stylesheet" href="https://unpkg.com/@uncinq/css-components">
56
48
  ```
57
49
 
58
- ---
50
+ ## What's included
59
51
 
60
- ## Installation
52
+ | Group | Components |
53
+ | --- | --- |
54
+ | Buttons | `.btn` with colour, size and style variants, plus `btn-close`, `btn-menu`, `btn-search`, `btn-filter`, `btn-share`, `btn-toc`, `btn-toggle-video` |
55
+ | Overlays | `modal`, `drawer`, `dropdown`, and the shared panel skeleton |
56
+ | Content | `alert`, `badge`, `banner`, `card`, `items`, `list`, `media`, `surtitle` |
57
+ | Navigation | `nav`, `nav-title`, `nav-accessibility`, `breadcrumb`, `pagination` |
58
+ | Embeds | `embed`, `video`, `map` |
59
+ | Forms | `form`, `form-check` and the layout helpers |
60
+ | Utilities | `scrollsnap` |
61
61
 
62
- ```bash
63
- npm install @uncinq/css-components
64
- # or
65
- yarn add @uncinq/css-components
66
- ```
62
+ ## Two import-order rules
67
63
 
68
- ### Usage — full import
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.
69
65
 
70
- ```css
71
- @import '@uncinq/design-tokens';
72
- @import '@uncinq/component-tokens';
73
- @import '@uncinq/css-base';
74
- @import '@uncinq/css-components';
75
- ```
66
+ `utilities/scrollsnap.css` comes after everything, in `@layer utilities`, because it has to win over the grid a component declares for itself.
76
67
 
77
- ### Usage — per component
68
+ If you import file by file rather than using the barrel, preserve both positions.
78
69
 
79
- ```css
80
- @import '@uncinq/design-tokens';
81
- @import '@uncinq/component-tokens';
82
- @import '@uncinq/css-base';
83
- @import '@uncinq/css-components/css/components/nav.css';
84
- @import '@uncinq/css-components/css/components/pagination.css';
85
- @import '@uncinq/css-components/css/utilities/scrollsnap.css';
86
- ```
70
+ ## Not included
87
71
 
88
- ### Usage — CDN (no build step)
72
+ - **No JavaScript.** `modal`, `drawer` and `dropdown` expect a script; each documents the classes and attributes it must toggle.
73
+ - **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.
89
75
 
90
- ```html
91
- <link rel="stylesheet" href="https://unpkg.com/@uncinq/design-tokens/tokens/index.css">
92
- <link rel="stylesheet" href="https://unpkg.com/@uncinq/component-tokens/tokens/index.css">
93
- <link rel="stylesheet" href="https://unpkg.com/@uncinq/css-base/css/index.css">
94
- <link rel="stylesheet" href="https://unpkg.com/@uncinq/css-components/css/index.css">
95
- ```
76
+ ## Documentation
96
77
 
97
- ---
98
-
99
- ## File structure
100
-
101
- ```text
102
- css/
103
- index.css ← imports all component and utility files
104
- components/
105
- alert.css ← alert / notification banner
106
- badge.css ← badge / pill / tag
107
- banner.css ← full-width banner strip
108
- breadcrumb.css ← breadcrumb navigation
109
- button.css ← button (all variants)
110
- card.css ← card container
111
- drawer.css ← off-canvas panel / drawer
112
- dropdown.css ← dropdown menu
113
- embed.css ← video / iframe embed wrapper
114
- list.css ← styled list
115
- map.css ← embedded map
116
- media.css ← media object (image + text)
117
- modal.css ← centered dialog / modal
118
- nav.css ← navigation bar
119
- nav-accessibility.css ← accessibility skip links / focus helpers
120
- nav-title.css ← navigation bar title
121
- pagination.css ← pagination control
122
- video.css ← native video wrapper with its play/pause control
123
- utilities/
124
- scrollsnap.css ← .scrollsnap[-sm|-md|-lg|-xl] — turns a grid into a snapping carousel
125
- ```
78
+ Full documentation: **[socle.uncinq.dev/docs/css-components/](https://socle.uncinq.dev/docs/css-components/)**
126
79
 
127
- ---
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`:
81
+
82
+ - [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
128
90
 
129
91
  ## References
130
92
 
131
- - [@uncinq/design-tokens](https://github.com/uncinq/design-tokens) — primitive + semantic layers
132
- - [@uncinq/component-tokens](https://github.com/uncinq/component-tokens) — component token layer
133
- - [@uncinq/css-base](https://github.com/uncinq/css-base) — base CSS rules
93
+ - [`@uncinq/component-tokens`](https://github.com/uncinq/component-tokens) — the values these components read
94
+ - [`@uncinq/css-base`](https://github.com/uncinq/css-base) — the foundation underneath
134
95
  - [MDN: CSS cascade layers](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Cascade_layers)
96
+
97
+ ## License
98
+
99
+ MIT © [Un Cinq](https://uncinq.dev/)
@@ -1,8 +1,12 @@
1
1
  /* components/card.css */
2
2
  /*
3
- * .card is a backwards-compatible alias for .item.
4
- * Prefer using .item directly: <article class="item page">
5
- *
3
+ * .card — self-contained content card.
4
+ *
5
+ * Historically a backwards-compatible alias for .item, but .item is NOT
6
+ * defined in this package (it lives in the Hugolify design system theme).
7
+ * Within @uncinq/css-components, .card is the complete implementation and the
8
+ * class to use. It reads --card-* from @uncinq/component-tokens.
9
+ *
6
10
  * Structure:
7
11
  * <article class="card">
8
12
  * <div class="content">
@@ -29,6 +29,8 @@
29
29
  --panel-color-background: var(--drawer-color-background);
30
30
  --panel-color-border: var(--drawer-color-border);
31
31
  --panel-color-text: var(--drawer-color-text);
32
+ --panel-color-text-active: var(--drawer-color-text-active);
33
+ --panel-color-text-hover: var(--drawer-color-text-hover);
32
34
  --panel-content-padding-block: var(--drawer-content-padding-block, var(--drawer-padding-block));
33
35
  --panel-content-padding-inline: var(--drawer-content-padding-inline, var(--drawer-padding-inline));
34
36
  --panel-footer-padding-block: var(--drawer-footer-padding-block, var(--drawer-padding-block));
@@ -1,10 +1,14 @@
1
1
  /* components/items.css */
2
2
  /*
3
- * .items — responsive grid.
3
+ * .items — responsive grid. Columns are intrinsic, not breakpoint-driven:
4
+ * children wrap once they no longer fit at --items-min-width.
5
+ *
6
+ * The children are yours to provide. Within this package that is .card;
7
+ * .item is defined by the Hugolify design system theme, not here.
4
8
  *
5
9
  * Structure:
6
10
  * .items
7
- * .item
11
+ * .card | .item | any child
8
12
  */
9
13
  @layer components {
10
14
  .items {
@@ -27,6 +27,8 @@
27
27
  --panel-color-background: var(--modal-color-background);
28
28
  --panel-color-border: var(--modal-color-border);
29
29
  --panel-color-text: var(--modal-color-text);
30
+ --panel-color-text-active: var(--modal-color-text-active);
31
+ --panel-color-text-hover: var(--modal-color-text-hover);
30
32
  --panel-content-padding-block: var(--modal-content-padding-block, var(--modal-padding-block));
31
33
  --panel-content-padding-inline: var(--modal-content-padding-inline, var(--modal-padding-inline));
32
34
  --panel-footer-padding-block: var(--modal-footer-padding-block, var(--modal-padding-block));
@@ -60,8 +60,10 @@
60
60
 
61
61
  :where(.modal, .drawer) {
62
62
  --color-link: var(--panel-color-text);
63
- --color-link-hover: var(--panel-color-text);
63
+ --color-link-active: var(--panel-color-text-active);
64
+ --color-link-hover: var(--panel-color-text-hover);
64
65
  --nav-color-text: var(--color-link);
66
+ --nav-color-text-active: var(--color-link-active);
65
67
 
66
68
  background-color: var(--panel-color-background);
67
69
  border: 0;
@@ -167,13 +169,15 @@
167
169
  of the panel's chrome and takes the page's own text and link colours.
168
170
  `inherit` on a custom property hands it back to the parent, which is
169
171
  exactly what is wanted here — and it has to be done token by token.
170
- Resetting --panel-color-text, which the three derive from, would make
172
+ Resetting the --panel-color-text-* tokens they derive from would make
171
173
  them invalid at computed-value time rather than page-valued, and every
172
174
  link would fall back to the surrounding text colour instead of the
173
175
  site's link colour. Keep this list in step with the panel rule above. */
174
176
  --color-link: inherit;
177
+ --color-link-active: inherit;
175
178
  --color-link-hover: inherit;
176
179
  --nav-color-text: inherit;
180
+ --nav-color-text-active: inherit;
177
181
 
178
182
  background-color: transparent;
179
183
  block-size: auto;
@@ -214,13 +218,15 @@
214
218
  of the panel's chrome and takes the page's own text and link colours.
215
219
  `inherit` on a custom property hands it back to the parent, which is
216
220
  exactly what is wanted here — and it has to be done token by token.
217
- Resetting --panel-color-text, which the three derive from, would make
221
+ Resetting the --panel-color-text-* tokens they derive from would make
218
222
  them invalid at computed-value time rather than page-valued, and every
219
223
  link would fall back to the surrounding text colour instead of the
220
224
  site's link colour. Keep this list in step with the panel rule above. */
221
225
  --color-link: inherit;
226
+ --color-link-active: inherit;
222
227
  --color-link-hover: inherit;
223
228
  --nav-color-text: inherit;
229
+ --nav-color-text-active: inherit;
224
230
 
225
231
  background-color: transparent;
226
232
  block-size: auto;
@@ -261,13 +267,15 @@
261
267
  of the panel's chrome and takes the page's own text and link colours.
262
268
  `inherit` on a custom property hands it back to the parent, which is
263
269
  exactly what is wanted here — and it has to be done token by token.
264
- Resetting --panel-color-text, which the three derive from, would make
270
+ Resetting the --panel-color-text-* tokens they derive from would make
265
271
  them invalid at computed-value time rather than page-valued, and every
266
272
  link would fall back to the surrounding text colour instead of the
267
273
  site's link colour. Keep this list in step with the panel rule above. */
268
274
  --color-link: inherit;
275
+ --color-link-active: inherit;
269
276
  --color-link-hover: inherit;
270
277
  --nav-color-text: inherit;
278
+ --nav-color-text-active: inherit;
271
279
 
272
280
  background-color: transparent;
273
281
  block-size: auto;
@@ -308,13 +316,15 @@
308
316
  of the panel's chrome and takes the page's own text and link colours.
309
317
  `inherit` on a custom property hands it back to the parent, which is
310
318
  exactly what is wanted here — and it has to be done token by token.
311
- Resetting --panel-color-text, which the three derive from, would make
319
+ Resetting the --panel-color-text-* tokens they derive from would make
312
320
  them invalid at computed-value time rather than page-valued, and every
313
321
  link would fall back to the surrounding text colour instead of the
314
322
  site's link colour. Keep this list in step with the panel rule above. */
315
323
  --color-link: inherit;
324
+ --color-link-active: inherit;
316
325
  --color-link-hover: inherit;
317
326
  --nav-color-text: inherit;
327
+ --nav-color-text-active: inherit;
318
328
 
319
329
  background-color: transparent;
320
330
  block-size: auto;
package/docs/_index.md ADDED
@@ -0,0 +1,89 @@
1
+ ---
2
+ isIndex: false
3
+ title: css-components
4
+ description: Framework-agnostic CSS component implementations, driven entirely by component tokens and scoped to @layer components.
5
+ weight: 5
6
+ icon: grid
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, vendors, base, 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
+ Without a build step:
60
+
61
+ ```html
62
+ <link rel="stylesheet" href="https://unpkg.com/@uncinq/css-components">
63
+ ```
64
+
65
+ Note that `@uncinq/css-base` is a genuine prerequisite but is **not** declared in `peerDependencies`. Install it explicitly.
66
+
67
+ ## Import order inside the package
68
+
69
+ `css/index.css` imports the components alphabetically, with two deliberate exceptions.
70
+
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.
72
+
73
+ `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
+
75
+ If you import file by file rather than using the barrel, preserve those two positions.
76
+
77
+ ## What this package does not include
78
+
79
+ **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.
80
+
81
+ **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
+
83
+ **No icons.** Components that show a glyph, such as `.pagination` controls, expect the theme to supply it through `::before` or `::after` content.
84
+
85
+ ## References
86
+
87
+ - [@uncinq/component-tokens](https://github.com/uncinq/component-tokens), the values these components read
88
+ - [@uncinq/css-base](https://github.com/uncinq/css-base), the foundation underneath
89
+ - [MDN: CSS cascade layers](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Cascade_layers)
@@ -0,0 +1,103 @@
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
+ ## `.btn`
10
+
11
+ The base class. Works on `<button>`, `<a>` and `<input type="submit">`, and reads `--btn-*` from [@uncinq/component-tokens](../../component-tokens/reference/).
12
+
13
+ ```html
14
+ <button class="btn">Send</button>
15
+ <a class="btn btn-secondary" href="/contact">Contact</a>
16
+ ```
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 variant sets `--btn-color-background`, `--btn-color-border` and `--btn-color-text` together. The text colour always comes from the matching `--color-text-on-*` semantic token, so contrast is preserved when the underlying colour is overridden. That pairing is the reason to change `--color-brand` rather than `--btn-color-background` when rebranding.
34
+
35
+ ### Sizes
36
+
37
+ | Class | Use |
38
+ | --- | --- |
39
+ | `.btn-xs` | Dense UI, inline controls |
40
+ | `.btn-sm` | Compact |
41
+ | `.btn-md` | Default, can be omitted |
42
+ | `.btn-lg` | Prominent |
43
+ | `.btn-xl` | Hero call to action |
44
+
45
+ ### Style variants
46
+
47
+ | Class | Effect |
48
+ | --- | --- |
49
+ | `.btn-ghost` | Transparent background, visible border |
50
+ | `.btn-link` | Renders as a link, no background or border |
51
+ | `.btn-control` | Form control styling, transparent border, for buttons sitting next to inputs |
52
+
53
+ Variants compose: `class="btn btn-danger btn-sm btn-ghost"`.
54
+
55
+ ## Specialised buttons
56
+
57
+ Seven single-purpose buttons. Read the second column before using one: whether `.btn` is required differs between them.
58
+
59
+ | Class | Needs `.btn` | Icon | Purpose |
60
+ | --- | --- | --- | --- |
61
+ | `.btn-close` | no | `--icon-close` | Close control, used inside panels and alerts |
62
+ | `.btn-menu` | no | `--icon-menu` | Menu toggle, typically the burger in a header |
63
+ | `.btn-search` | yes | `--icon-search` | Search toggle |
64
+ | `.btn-filter` | yes | `--icon-filter` | Filters toggle |
65
+ | `.btn-share` | yes | `--icon-share` | Share control |
66
+ | `.btn-toc` | yes | `--icon-toc` | Table of contents toggle |
67
+ | `.btn-toggle-video` | yes | child elements | Play and pause control overlaid on a video |
68
+
69
+ ```html
70
+ <!-- self-contained -->
71
+ <button class="btn-close" aria-label="Close"></button>
72
+
73
+ <!-- modifier on .btn -->
74
+ <button class="btn btn-search" aria-label="Search"></button>
75
+ ```
76
+
77
+ `.btn-close` and `.btn-menu` redeclare everything they need, with `--btn-close-*` and `--btn-menu-*` falling back to the matching `--btn-*` token. They therefore look consistent with `.btn` without depending on it. The other five are modifiers, and omitting the base class leaves them largely unstyled.
78
+
79
+ Six of the seven **do ship their icon**, drawn as a `mask` on `::before` from the icon tokens in [@uncinq/design-tokens](../../design-tokens/reference/#icon). Because it is a mask rather than a background image, the glyph takes `currentColor` and follows the button's text colour through every state. Size it with `--icon-size`, or per button with `--btn-close-icon-size` and its siblings.
80
+
81
+ `.btn-toggle-video` is the exception and expects two child elements, for the reason given below.
82
+
83
+ 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). None of these buttons provides one for you.
84
+
85
+ ## `.btn-toggle-video`
86
+
87
+ The one worth explaining, because its behaviour is driven by a state class rather than by CSS alone.
88
+
89
+ It is a play and pause control overlaid on a video that autoplays without native controls, which WCAG requires to be pausable. It stays transparent so the video shows through, and the `.is-playing` class, toggled by whatever script drives the video, decides which of the two icons is shown.
90
+
91
+ ```html
92
+ <div class="video">
93
+ <video autoplay muted loop playsinline></video>
94
+ <button class="btn btn-toggle-video is-playing" aria-label="Pause">
95
+ <span class="icon-pause"></span>
96
+ <span class="icon-play"></span>
97
+ </button>
98
+ </div>
99
+ ```
100
+
101
+ The `.video` wrapper is what positions it. See [Embeds](../embeds/).
102
+
103
+ Your script owns two things: toggling `.is-playing` in step with the video's actual state, and keeping the accessible name in sync, since the control means "pause" while playing and "play" while paused.
@@ -0,0 +1,75 @@
1
+ ---
2
+ isIndex: false
3
+ title: Cascade layers
4
+ description: The two layers this package owns, why panel.css is imported last, and how to override a component cleanly.
5
+ weight: 1
6
+ icon: stack
7
+ ---
8
+
9
+ This package writes to two layers.
10
+
11
+ | Layer | Contents |
12
+ | --- | --- |
13
+ | `@layer components` | All 29 components |
14
+ | `@layer utilities` | `.scrollsnap`, the only utility |
15
+
16
+ The full order, including layers owned by the sibling packages and by the project itself, is the **consuming project's** responsibility. Declare it once, at the very top of your entry stylesheet, before any `@import`:
17
+
18
+ ```css
19
+ @layer reset, tokens, libs, vendors, base, layouts, components, pages, utilities;
20
+ ```
21
+
22
+ CSS fixes a layer's position the first time its name is seen, and later re-declarations do not reorder it. If that line comes after an import, it is already too late. See [the css-base page](../../css-base/cascade-layers/) for the full explanation, including why `libs` and `vendors` are a pair.
23
+
24
+ ## Why `utilities` is last
25
+
26
+ `.scrollsnap` changes the `grid-template-columns` that a component such as `.items` declares for itself in `@layer components`. A later layer beats an earlier one regardless of specificity, so putting it in `utilities` is what lets a single class opt an existing component into carousel behaviour without rewriting the component.
27
+
28
+ This is also why it is a utility rather than a component rule: it is a modifier applied to something else, not a thing of its own.
29
+
30
+ ## Why `panel.css` is imported last
31
+
32
+ Inside `@layer components`, source order breaks ties between rules of equal specificity. Two facts combine here:
33
+
34
+ 1. Most of `panel.css` is wrapped in `:where()`, so it carries **zero specificity**. That is deliberate: it lets `.modal`, `.drawer` and your own overrides win without escalating.
35
+ 2. The `.panel-inline-*` variants must beat `.modal` and `.drawer`, and they are plain class selectors.
36
+
37
+ Because those variants have no specificity advantage over the host components, they rely on arriving later in the file. Hence `@import 'components/panel.css';` sits last among the component imports in `css/index.css`.
38
+
39
+ If you import files individually, keep `panel.css` after `modal.css` and `drawer.css`.
40
+
41
+ ## Overriding a component
42
+
43
+ Write to the same layer after the imports:
44
+
45
+ ```css
46
+ @layer components {
47
+ .alert {
48
+ --alert-border-radius: 0;
49
+ }
50
+ }
51
+ ```
52
+
53
+ Prefer redefining the token over rewriting the declaration. Almost every value in this package comes from a `--component-*` custom property, so an override usually needs no selector work at all.
54
+
55
+ Better still, when the change is contextual rather than global, set the property on an ancestor and let it inherit:
56
+
57
+ ```css
58
+ .sidebar {
59
+ --alert-padding-inline: var(--spacing-sm);
60
+ }
61
+ ```
62
+
63
+ That is a normal declaration on a normal selector, competing on specificity like any other rule, not on layer order. It restyles a region without adding a variant class to the system.
64
+
65
+ ## Overriding a `:where()` rule
66
+
67
+ Because `panel.css` uses `:where()` throughout, its rules have zero specificity and a single class selector of yours already outranks them:
68
+
69
+ ```css
70
+ .modal {
71
+ border-radius: 0;
72
+ }
73
+ ```
74
+
75
+ No `!important`, no repeated class, no `:is()` trick. This is the main practical benefit of the `:where()` convention, and it is worth preserving if you contribute to the package.
@@ -0,0 +1,145 @@
1
+ ---
2
+ isIndex: false
3
+ title: Content
4
+ description: Alerts, badges, banners, cards, the items grid, labelled lists, media objects and surtitles.
5
+ weight: 4
6
+ icon: card-text
7
+ ---
8
+
9
+ ## `.alert`
10
+
11
+ An inline notification block.
12
+
13
+ ```html
14
+ <div class="alert alert-info">
15
+ <p class="alert-heading">Heads up</p>
16
+ <p>Your changes were saved.</p>
17
+ </div>
18
+ ```
19
+
20
+ Variants: `.alert-primary`, `.alert-secondary`, `.alert-brand`, `.alert-neutral`, `.alert-dark`, `.alert-light`, `.alert-success`, `.alert-danger`, `.alert-warning`, `.alert-info`.
21
+
22
+ Each sets the background, border and text colour from the matching `-muted` and `-strong` semantic tokens, so the pairing stays legible if the underlying colour changes.
23
+
24
+ Two behaviours are worth knowing. `--color-link` is reassigned to the alert's own text colour, so links inside an alert do not fall back to the page link colour against a tinted background. And an alert whose first child is an `.icon`, with no `.alert-heading`, switches to a horizontal layout automatically.
25
+
26
+ `.container` inside an alert inherits the same flex layout, which is what lets a full-bleed alert keep its content aligned with the rest of the page.
27
+
28
+ ## `.banner`
29
+
30
+ A thin variation on the alert, centred rather than start-aligned. It sets `--alert-align: center` and flattens paragraph margins, and nothing else.
31
+
32
+ ```html
33
+ <div class="alert banner">
34
+ <p>Site-wide announcement.</p>
35
+ </div>
36
+ ```
37
+
38
+ It is a modifier on `.alert`, so use both classes.
39
+
40
+ ## `.badge`
41
+
42
+ A small inline label.
43
+
44
+ ```html
45
+ <span class="badge badge-success">Published</span>
46
+ <a class="badge" href="/tag/css">css</a>
47
+ ```
48
+
49
+ Variants mirror the alert set: `.badge-primary`, `.badge-secondary`, `.badge-brand`, `.badge-neutral`, `.badge-dark`, `.badge-light`, `.badge-success`, `.badge-danger`, `.badge-warning`, `.badge-info`.
50
+
51
+ Hover styles apply only to `a.badge`, guarded by `@media (hover: hover) and (pointer: fine)`, so a static badge never looks interactive.
52
+
53
+ ## `.card`
54
+
55
+ A self-contained content card.
56
+
57
+ ```html
58
+ <article class="card">
59
+ <div class="content">
60
+ <p class="surtitle">Category</p>
61
+ <p class="title">Card title</p>
62
+ <p class="description">Short description.</p>
63
+ </div>
64
+ <div class="media">
65
+ <picture>...</picture>
66
+ </div>
67
+ </article>
68
+ ```
69
+
70
+ The media block sits first visually, through `--card-media-order: -1`, regardless of its position in the markup. Put it after the content so that screen readers and search engines reach the text first.
71
+
72
+ Wrapping the content in `a.content` makes the whole card clickable, using a `::before` overlay that covers the card. Focus is then handled on the card itself with `:has(a.content:focus-visible)`, so the focus ring outlines the card rather than an invisible box. Nested interactive elements inside such a card will be unreachable, which is the known trade-off of this pattern.
73
+
74
+ Sizing with `--card-max-width` and `--card-width` is unset by default. Prefer `ch` units, so the width tracks the text measure.
75
+
76
+ {{< alert-block state="info" >}}
77
+ The source comment in `card.css` describes `.card` as a backwards-compatible alias for `.item` and recommends using `.item` directly. That is misleading for anyone consuming this package on its own: **`.item` is not defined here**. It lives in the Hugolify design system theme. Within `@uncinq/css-components`, `.card` is a complete implementation and the class to use.
78
+ {{< /alert-block >}}
79
+
80
+ ## `.items`
81
+
82
+ A responsive grid, and the usual container for cards.
83
+
84
+ ```html
85
+ <div class="items">
86
+ <article class="card">...</article>
87
+ <article class="card">...</article>
88
+ </div>
89
+ ```
90
+
91
+ The column count is intrinsic rather than breakpoint-driven:
92
+
93
+ ```css
94
+ grid-template-columns: repeat(
95
+ var(--items-cols, auto-fill),
96
+ minmax(min(var(--items-min-width, var(--max-width-item)), 100%), 1fr)
97
+ );
98
+ ```
99
+
100
+ Items wrap when they no longer fit at `--items-min-width`, with no media query involved. Set `--items-cols` to a number to force a fixed count instead.
101
+
102
+ It also declares `container-type: inline-size` and `container-name: items`, so children can use container queries against the grid rather than the viewport.
103
+
104
+ Pair it with [`.scrollsnap`](../utilities/) to turn the grid into a horizontal carousel below a given breakpoint.
105
+
106
+ ## `.list`
107
+
108
+ A compact labelled list with dash markers.
109
+
110
+ ```html
111
+ <div class="list">
112
+ <p>Optional label</p>
113
+ <ul>
114
+ <li>First</li>
115
+ <li>Second</li>
116
+ </ul>
117
+ </div>
118
+ ```
119
+
120
+ The wrapper is a `div`, with the label as a sibling of the list rather than inside it. Either `ul` or `ol` works.
121
+
122
+ ## `.media`
123
+
124
+ The structural base for media blocks, controlling aspect ratio, background and border.
125
+
126
+ | Class | Use |
127
+ | --- | --- |
128
+ | `.media` | Base, for a picture or video |
129
+ | `.media-logo` | Logo, typically contained rather than cropped |
130
+ | `.media-icon` | Icon, sized from `--icon-size` |
131
+
132
+ `.media-logo` and `.media-icon` build on `.media`. Inside a `.card`, the card reassigns `--media-color-background` and `--media-ratio` from its own `--card-media-*` tokens, so a media block adapts to its context without a variant class.
133
+
134
+ A `.video` inside a `.media` is stretched to fill it. See [Embeds](../embeds/).
135
+
136
+ ## `.surtitle`
137
+
138
+ An eyebrow label above a heading.
139
+
140
+ ```html
141
+ <p class="surtitle">Category</p>
142
+ <h2>Main title</h2>
143
+ ```
144
+
145
+ Inside a `.card` it gets a negative `--surtitle-margin-block-end`, pulling it closer to the title so the pair reads as one unit.
package/docs/embeds.md ADDED
@@ -0,0 +1,66 @@
1
+ ---
2
+ isIndex: false
3
+ title: Embeds
4
+ description: Aspect-ratio wrappers for third-party embeds, native video and maps.
5
+ weight: 6
6
+ icon: play-btn
7
+ ---
8
+
9
+ ## `.embed`
10
+
11
+ A responsive container for third-party embeds, holding an aspect ratio so the page does not reflow once the iframe loads.
12
+
13
+ ```html
14
+ <div class="embed">
15
+ <iframe src="https://..." title="Video title" allowfullscreen></iframe>
16
+ </div>
17
+ ```
18
+
19
+ Accepts `iframe`, `video`, `embed` or `object` as its child. The ratio comes from `--embed-ratio`, which defaults to the video ratio token. Override it per instance for a square or portrait embed:
20
+
21
+ ```html
22
+ <div class="embed" style="--embed-ratio: 1">...</div>
23
+ ```
24
+
25
+ Always give the `iframe` a `title`. It is the only accessible name an embedded frame has.
26
+
27
+ ## `.video`
28
+
29
+ A wrapper for a **native** `<video>`, as opposed to an embedded player.
30
+
31
+ ```html
32
+ <div class="video">
33
+ <video autoplay muted loop playsinline></video>
34
+ <button class="btn btn-toggle-video is-playing" aria-label="Pause">
35
+ <span class="icon-pause"></span>
36
+ <span class="icon-play"></span>
37
+ </button>
38
+ </div>
39
+ ```
40
+
41
+ The wrapper positions the optional toggle control over the video. Inside a `.media`, the wrapper is stretched to fill it, so a video and an image are interchangeable within a card.
42
+
43
+ The control is there for a reason rather than for decoration. A video that autoplays without native controls must be pausable to meet WCAG, which is exactly what `.btn-toggle-video` provides. If your video carries `controls`, you do not need it. See [Buttons](../buttons/#btn-toggle-video) for the `.is-playing` contract.
44
+
45
+ ## `.map`
46
+
47
+ A map container, driven by Leaflet through a `js-map` hook.
48
+
49
+ ```html
50
+ <div class="map js-map"
51
+ data-markers="[...]"
52
+ data-zoom="12"
53
+ data-tile="3"
54
+ data-marker-hidden="false"></div>
55
+ ```
56
+
57
+ | Attribute | Role |
58
+ | --- | --- |
59
+ | `data-markers` | The markers to plot |
60
+ | `data-zoom` | Initial zoom level |
61
+ | `data-tile` | Tile layer selection |
62
+ | `data-marker-hidden` | Whether markers start hidden |
63
+
64
+ This package provides the container only: sizing, background and border. Leaflet itself, its stylesheet and the script reading those attributes are yours to supply.
65
+
66
+ Leaflet's own CSS is a good example of why the layer order matters. Import it into `@layer libs` and any adjustments you write into `@layer vendors`, so that a lazily loaded Leaflet cannot outrank your own rules on source order. See [Cascade layers](../cascade-layers/).
package/docs/forms.md ADDED
@@ -0,0 +1,80 @@
1
+ ---
2
+ isIndex: false
3
+ title: Forms
4
+ description: The .form grid, its layout helpers, and where the actual control styling lives.
5
+ weight: 7
6
+ icon: ui-checks
7
+ ---
8
+
9
+ Form **controls** are styled by [@uncinq/css-base](../../css-base/base/#forms), on the native elements themselves. There is no `.input` or `.select` class to apply.
10
+
11
+ This package adds only what css-base cannot: the layout around those controls.
12
+
13
+ ## `.form`
14
+
15
+ A responsive two-column grid that collapses to one column when there is not enough room.
16
+
17
+ ```html
18
+ <form class="form">
19
+ <div>
20
+ <label for="first">First name</label>
21
+ <input id="first" type="text">
22
+ </div>
23
+ <div>
24
+ <label for="last">Last name</label>
25
+ <input id="last" type="text">
26
+ </div>
27
+ <div class="full">
28
+ <label for="message">Message</label>
29
+ <textarea id="message"></textarea>
30
+ <p class="help">Markdown is supported.</p>
31
+ </div>
32
+ <div class="actions">
33
+ <button class="btn" type="submit">Send</button>
34
+ </div>
35
+ </form>
36
+ ```
37
+
38
+ The column rule is worth reading once:
39
+
40
+ ```css
41
+ grid-template-columns: repeat(
42
+ auto-fill,
43
+ minmax(max(var(--form-col-min-width, 25ch), calc(50% - var(--gap) / 2)), 1fr)
44
+ );
45
+ ```
46
+
47
+ The `max()` sets a floor of `25ch` **and** caps the result at half the width. The effect is a form that is one column or two, never three, however wide the container gets. A three-column form is almost never what you want, because it breaks the vertical reading order of a sequence of fields.
48
+
49
+ Change the floor with `--form-col-min-width`.
50
+
51
+ ## Layout helpers
52
+
53
+ | Class | Effect |
54
+ | --- | --- |
55
+ | `.full` | Spans both columns, `grid-column: 1 / -1` |
56
+ | `.actions` | Spans both columns and stacks its children vertically |
57
+ | `.help` | Help text under a field |
58
+ | `.form-check` | A checkbox or radio laid out inline with its label |
59
+
60
+ ```html
61
+ <div class="form-check">
62
+ <input id="terms" type="checkbox">
63
+ <label for="terms">I accept the terms</label>
64
+ </div>
65
+ ```
66
+
67
+ `.form-check` is deliberately minimal, just `display: flex`. The checkbox and radio appearance, including the gap with the adjacent label, comes from css-base, which styles `[type='checkbox'] + label` directly.
68
+
69
+ ## Accessibility
70
+
71
+ Nothing here supplies an accessible name. Every control needs a `<label for>` pointing at its `id`, or a `.visually-hidden` label when the design has no room for a visible one.
72
+
73
+ Tie `.help` text to its field with `aria-describedby`, otherwise it is visible but never announced:
74
+
75
+ ```html
76
+ <input id="message" aria-describedby="message-help">
77
+ <p class="help" id="message-help">Markdown is supported.</p>
78
+ ```
79
+
80
+ For error states, use `aria-invalid="true"` on the control and point `aria-describedby` at the error message.
@@ -0,0 +1,93 @@
1
+ ---
2
+ isIndex: false
3
+ title: Navigation
4
+ description: Navigation lists, skip links, breadcrumbs and pagination.
5
+ weight: 5
6
+ icon: signpost
7
+ ---
8
+
9
+ ## `.nav`
10
+
11
+ The base navigation list, vertical by default.
12
+
13
+ ```html
14
+ <ul class="nav">
15
+ <li><a href="/">Home</a></li>
16
+ <li><a href="/about">About</a></li>
17
+ </ul>
18
+ ```
19
+
20
+ Works on `ul` or `ol`. **Direction is set by context, not by a modifier class**: a header lays its nav out horizontally by setting `flex-direction: row` on it. That keeps the component free of layout variants that would only ever be used in one place.
21
+
22
+ ## `.nav-title`
23
+
24
+ A title within a navigation block, for labelling a group of links.
25
+
26
+ ## `.nav-accessibility`
27
+
28
+ Skip links, for keyboard and assistive technology users.
29
+
30
+ ```html
31
+ <ul class="nav nav-accessibility">
32
+ <li><a href="#main">Skip to content</a></li>
33
+ <li><a href="#navigation">Skip to navigation</a></li>
34
+ <li><a href="#footer">Skip to footer</a></li>
35
+ </ul>
36
+ ```
37
+
38
+ Three things matter here, and getting any of them wrong defeats the purpose:
39
+
40
+ **It must be the first focusable element in `<body>`.** A skip link that comes after the navigation it skips is useless.
41
+
42
+ **It is hidden off-screen, not hidden.** It slides into view when a child receives focus. Using `display: none` would remove it from the tab order entirely.
43
+
44
+ **It requires the `.nav` base class** alongside it.
45
+
46
+ The targets must exist and be focusable. An `id` on a non-interactive element needs `tabindex="-1"` for the jump to move focus rather than just scroll.
47
+
48
+ ## `.breadcrumb-wrapper`
49
+
50
+ The breadcrumb trail. Note that the class goes on the **wrapper**, not on the list itself.
51
+
52
+ ```html
53
+ <nav class="breadcrumb-wrapper" aria-label="Breadcrumb">
54
+ <ol>
55
+ <li><a href="/">Home</a></li>
56
+ <li><a href="/docs">Docs</a></li>
57
+ <li aria-current="page">Navigation</li>
58
+ </ol>
59
+ </nav>
60
+ ```
61
+
62
+ The separator is drawn from `--breadcrumb-separator`, which defaults to `"/"`. Mark the current page with `aria-current="page"` rather than relying on it being last.
63
+
64
+ ## `.pagination`
65
+
66
+ A page navigation list.
67
+
68
+ ```html
69
+ <nav role="navigation" aria-label="Pagination">
70
+ <ul class="pagination">
71
+ <li class="disabled"><a class="first">First</a></li>
72
+ <li><a class="previous">Previous</a></li>
73
+ <li class="item active"><a href="?page=1">1</a></li>
74
+ <li class="item item-adjacent"><a href="?page=2">2</a></li>
75
+ <li><a class="next">Next</a></li>
76
+ <li><a class="last">Last</a></li>
77
+ </ul>
78
+ </nav>
79
+ ```
80
+
81
+ | Class | Role |
82
+ | --- | --- |
83
+ | `.first`, `.previous`, `.next`, `.last` | Navigation controls, on the `a` |
84
+ | `.item` | A page number, on the `li` |
85
+ | `.item-adjacent` | A page number next to the current one |
86
+ | `.active` | The current page |
87
+ | `.disabled` | An unavailable control, on the `li` |
88
+
89
+ `.item-adjacent` exists so that a responsive paginator can hide distant page numbers while keeping the immediate neighbours visible, which is the usual mobile treatment.
90
+
91
+ **The glyphs for `.first`, `.last`, `.previous` and `.next` are not provided.** This package positions and spaces them; the theme supplies the arrow through `a::before` or `a::after` content. A paginator with no theme styling will show the text labels only, which is a reasonable fallback but rarely what you want visually.
92
+
93
+ Note that `.item` here means a pagination entry, and is unrelated to the `.item` card unit mentioned in [Content](../content/).
@@ -0,0 +1,133 @@
1
+ ---
2
+ isIndex: false
3
+ title: Overlays
4
+ description: The shared panel skeleton behind .modal and .drawer, the two opening strategies, and the .dropdown contract.
5
+ weight: 3
6
+ icon: window-stack
7
+ ---
8
+
9
+ ## The shared panel skeleton
10
+
11
+ `.modal` and `.drawer` are the same panel. Same structure (header, content, footer), same open and closed contract, same backdrop. They differ only in where they sit: modal centres them, drawer sticks them to an edge.
12
+
13
+ `components/panel.css` owns every rule. The two variants are **pure configuration**: `modal.css` and `drawer.css` declare nothing but `--panel-*` tokens.
14
+
15
+ There is **no `.panel` class to apply.** The file targets `:where(.modal, .drawer)` directly, so the skeleton arrives with either variant class. `panel` names the shared concept and the token namespace, not a class.
16
+
17
+ ```html
18
+ <dialog class="modal" id="contact" aria-labelledby="contactLabel">
19
+ <div class="header">
20
+ <p class="title" id="contactLabel">Contact</p>
21
+ <button class="btn-close js-dialog-close" aria-label="Close"></button>
22
+ </div>
23
+ <div class="content">...</div>
24
+ <div class="footer">...</div>
25
+ </dialog>
26
+ ```
27
+
28
+ Each variant aliases `--panel-*` from its own namespace, so `--modal-*` and `--drawer-*` remain the public API. Override those, not `--panel-*`.
29
+
30
+ ## Two ways to open a panel
31
+
32
+ **The tag picks the strategy, not the class.** Either variant works both ways.
33
+
34
+ ### `<dialog>` with `showModal()`
35
+
36
+ The browser handles Escape, the focus trap and making the rest of the page inert. You get `[open]`, the native `::backdrop` and the top layer for free.
37
+
38
+ This is the right choice when the panel is **always** a dialog.
39
+
40
+ ### `popover="auto"` with `showPopover()`
41
+
42
+ Same top layer, same native `::backdrop`, but the element carries no implicit role.
43
+
44
+ Use this when the panel becomes a static region of the page above a breakpoint, which is what the `.panel-inline-*` variants do. The reason is an accessibility one: a `<dialog>` is exposed as `role="dialog"` even when it is only styled as a sidebar, whereas a script can set that role solely while the panel is actually open.
45
+
46
+ ## `.modal`
47
+
48
+ A centred panel. Size a single instance by overriding `--modal-max-width` on the element itself.
49
+
50
+ ```html
51
+ <dialog class="modal" style="--modal-max-width: 40rem">...</dialog>
52
+ ```
53
+
54
+ ## `.drawer`
55
+
56
+ A panel sliding in from an edge.
57
+
58
+ | Class | Edge |
59
+ | --- | --- |
60
+ | `.drawer-start` | Inline start, left in a left-to-right document |
61
+ | `.drawer-end` | Inline end |
62
+ | `.drawer-top` | Top |
63
+ | `.drawer-bottom` | Bottom |
64
+
65
+ ## `.panel-inline-*`
66
+
67
+ Turns a panel into an ordinary region of the page from a breakpoint upwards, while it stays an overlay below.
68
+
69
+ | Class | Becomes inline at |
70
+ | --- | --- |
71
+ | `.panel-inline-sm` | `--sm`, 768px |
72
+ | `.panel-inline-md` | `--md`, 1024px |
73
+ | `.panel-inline-lg` | `--lg`, 1440px |
74
+ | `.panel-inline-xl` | `--xl`, 1600px |
75
+
76
+ This is the case that calls for `popover="auto"` rather than `<dialog>`. A filters sidebar that is a drawer on mobile and a column on desktop should not announce itself as a dialog while it is a column.
77
+
78
+ These variants beat `.modal` and `.drawer` on **source order**, not specificity, which is why `panel.css` is imported last. See [Cascade layers](../cascade-layers/).
79
+
80
+ ## `.panel-trigger-*`
81
+
82
+ The companion to `.panel-inline-*`, applied to the **trigger button** rather than to the panel. It hides the trigger at the breakpoint where the panel stops being an overlay.
83
+
84
+ | Class | Trigger hidden from |
85
+ | --- | --- |
86
+ | `.panel-trigger-sm` | `--sm`, 768px |
87
+ | `.panel-trigger-md` | `--md`, 1024px |
88
+ | `.panel-trigger-lg` | `--lg`, 1440px |
89
+ | `.panel-trigger-xl` | `--xl`, 1600px |
90
+
91
+ Pair the suffixes: a panel carrying `.panel-inline-md` needs its button carrying `.panel-trigger-md`, otherwise a button that opens nothing remains on screen at desktop width.
92
+
93
+ The reason this is a second class rather than something CSS derives on its own is worth knowing, because it looks like a redundancy. Nothing structurally connects a trigger to its panel: the only link is `data-target` matching an `id`, and no selector can compare two attribute values. A positional rule would work only until somebody moved the markup, and a filter button typically lives in the page heading rather than beside the panel it opens. In practice both classes come from the same configuration value, so the pair cannot disagree.
94
+
95
+ ## The JavaScript contract
96
+
97
+ This package ships no JavaScript. `.modal` and `.drawer` expect a script that provides:
98
+
99
+ | Hook | Role |
100
+ | --- | --- |
101
+ | `.js-dialog-toggle[data-target="#id"]` | Opens the panel it points at |
102
+ | `.js-dialog-close` | Any element inside the panel that closes it |
103
+
104
+ Closing must also work on Escape and on a click outside. With `<dialog>` and `showModal()` the browser already does both. With `popover="auto"` the platform handles light dismiss and Escape too, so in practice the script mainly manages the role and the open call.
105
+
106
+ ## `.dropdown`
107
+
108
+ A contextual menu, with a tighter JavaScript contract because it animates.
109
+
110
+ ```html
111
+ <div class="dropdown">
112
+ <button class="js-dropdown-toggle" aria-expanded="false">Label</button>
113
+ <ul class="dropdown-menu" role="menu">
114
+ <li><a class="dropdown-item" href="#">Item</a></li>
115
+ <li><hr class="dropdown-divider"></li>
116
+ <li><a class="dropdown-item" href="#">Item</a></li>
117
+ </ul>
118
+ </div>
119
+ ```
120
+
121
+ The trigger is `.js-dropdown-toggle`, and its **next element sibling must be** `.dropdown-menu`.
122
+
123
+ The script toggles three classes on the menu, in this sequence:
124
+
125
+ | Class | Meaning |
126
+ | --- | --- |
127
+ | `.showing` | Opening transition has started |
128
+ | `.show` | Fully open |
129
+ | `.hiding` | Closing transition has started |
130
+
131
+ It must also keep `aria-expanded` on the toggle in sync. The three-class sequence exists so the menu can animate out before being removed from the flow; a single `.show` class would make the closing transition impossible.
132
+
133
+ `.dropdown-menu-end` aligns the menu to the inline end of its trigger rather than the start.
@@ -0,0 +1,67 @@
1
+ ---
2
+ isIndex: false
3
+ title: Utilities
4
+ description: The .scrollsnap utility, which turns a grid into a horizontal snap carousel below a breakpoint.
5
+ weight: 8
6
+ icon: tools
7
+ ---
8
+
9
+ ## `.scrollsnap`
10
+
11
+ Turns a grid into a horizontal carousel with snap points. Items keep their own width, the grid stops wrapping, and the row scrolls sideways, bleeding into the gutter so the next item peeks in.
12
+
13
+ ```html
14
+ <div class="items scrollsnap-md">
15
+ <article class="card">...</article>
16
+ <article class="card">...</article>
17
+ <article class="card">...</article>
18
+ </div>
19
+ ```
20
+
21
+ It is opt-in, applied to the element that owns the grid, so the same component is a carousel in one context and a plain grid in another.
22
+
23
+ ## The suffix names where the carousel stops
24
+
25
+ This is the one thing to get right, and it reads backwards from most responsive utilities.
26
+
27
+ | Class | Carousel below | Grid from |
28
+ | --- | --- | --- |
29
+ | `.scrollsnap` | every width | never |
30
+ | `.scrollsnap-sm` | `--sm`, 768px | 768px |
31
+ | `.scrollsnap-md` | `--md`, 1024px | 1024px |
32
+ | `.scrollsnap-lg` | `--lg`, 1440px | 1440px |
33
+ | `.scrollsnap-xl` | `--xl`, 1600px | 1600px |
34
+
35
+ **The suffix names the rung where the carousel STOPS and the grid comes back.** `.scrollsnap-md` is a carousel on mobile and tablet, and a grid from 1024px up.
36
+
37
+ ## Why it is a utility
38
+
39
+ It has to win over the `grid-template-columns` that a component such as `.items` declares for itself in `@layer components`. `@layer utilities` comes later in the order, so it wins regardless of specificity. See [Cascade layers](../cascade-layers/).
40
+
41
+ ## Item width
42
+
43
+ ```css
44
+ grid-auto-columns: min(
45
+ var(--items-min-width, var(--max-width-item)),
46
+ var(--items-carousel-max-width, 75vw)
47
+ );
48
+ ```
49
+
50
+ Items take `--items-min-width`, the same value that drives the grid, capped by `--items-carousel-max-width` (default `75vw`). The cap is what guarantees the next item always peeks in, which is the affordance that tells a reader the row scrolls at all. Without it, a wide item on a narrow screen would fill the viewport and the carousel would look like a static card.
51
+
52
+ ## The bleed, and why margin and padding disagree
53
+
54
+ The row pulls out to the screen edge using `--container-bleed`, published by `.container` in [css-base](../../css-base/layouts/#the-container-bleed-contract). The property inherits, so it reaches the row from a `.container` however far up it sits.
55
+
56
+ Outside a container there is no published value, and the two `var()` fallbacks differ **on purpose**, because margin and padding answer different questions:
57
+
58
+ | | Question | Fallback |
59
+ | --- | --- | --- |
60
+ | `margin` | How far may I pull out of my parent? | `0`, since nothing published a bleed. The parent may be a card or a column with no room to give, and pulling a gutter out of it would overflow |
61
+ | `padding` | How far in from my own edge should the first item sit? | `var(--gutter)`, the breathing room every other box gets, so a full-bleed block does not let the cards touch the screen |
62
+
63
+ Inside a container the two collapse to one value: both read the published bleed, and the padding puts back inside the scroll box exactly what the negative margin took out. The first item lands on the container's content edge, aligned with everything else on the page.
64
+
65
+ `scroll-margin-inline` follows the **padding**, not the margin. It is what makes an item snap flush with the content edge rather than the padding edge, so it has to cancel the padding whatever the margin does.
66
+
67
+ This is the same reasoning as the `--container-bleed` contract itself, applied to a scroll container. If you write your own full-bleed component, copy the pair rather than picking one fallback for both.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uncinq/css-components",
3
- "version": "1.15.3",
3
+ "version": "1.16.1",
4
4
  "description": "Framework-agnostic CSS component implementations.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,7 +19,8 @@
19
19
  "cascade-layers"
20
20
  ],
21
21
  "files": [
22
- "css/"
22
+ "css/",
23
+ "docs/"
23
24
  ],
24
25
  "exports": {
25
26
  ".": "./css/index.css",