@uncinq/css-components 1.16.2 → 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.
- package/README.md +10 -10
- package/css/components/alert.css +5 -0
- package/css/components/panel.css +8 -6
- package/docs/_index.md +13 -11
- package/docs/buttons/_index.md +75 -0
- package/docs/buttons/btn-close.md +58 -0
- package/docs/buttons/btn-filter.md +36 -0
- package/docs/buttons/btn-menu.md +39 -0
- package/docs/buttons/btn-search.md +40 -0
- package/docs/buttons/btn-share.md +33 -0
- package/docs/buttons/btn-toc.md +23 -0
- package/docs/buttons/btn-toggle-video.md +58 -0
- package/docs/buttons/btn.md +95 -0
- package/docs/cascade-layers.md +8 -6
- package/docs/content/_index.md +39 -0
- package/docs/content/alert.md +97 -0
- package/docs/content/badge.md +50 -0
- package/docs/content/banner.md +43 -0
- package/docs/content/card.md +93 -0
- package/docs/content/items.md +76 -0
- package/docs/content/list.md +41 -0
- package/docs/content/media.md +88 -0
- package/docs/content/surtitle.md +43 -0
- package/docs/embeds/_index.md +25 -0
- package/docs/embeds/embed.md +53 -0
- package/docs/embeds/map.md +57 -0
- package/docs/embeds/video.md +53 -0
- package/docs/forms/_index.md +31 -0
- package/docs/forms/form-check.md +77 -0
- package/docs/forms/form.md +67 -0
- package/docs/navigation/_index.md +29 -0
- package/docs/navigation/breadcrumb.md +59 -0
- package/docs/navigation/nav-accessibility.md +67 -0
- package/docs/navigation/nav-title.md +43 -0
- package/docs/navigation/nav.md +65 -0
- package/docs/navigation/pagination.md +77 -0
- package/docs/overlays/_index.md +34 -0
- package/docs/overlays/drawer.md +58 -0
- package/docs/overlays/dropdown.md +87 -0
- package/docs/overlays/modal.md +75 -0
- package/docs/overlays/panel.md +159 -0
- package/docs/utilities/_index.md +21 -0
- package/docs/{utilities.md → utilities/scrollsnap.md} +26 -9
- package/package.json +1 -1
- package/docs/buttons.md +0 -103
- package/docs/content.md +0 -145
- package/docs/embeds.md +0 -66
- package/docs/forms.md +0 -80
- package/docs/navigation.md +0 -93
- package/docs/overlays.md +0 -133
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Drawer
|
|
4
|
+
description: The edge-pinned panel configuration and its four position variants.
|
|
5
|
+
weight: 3
|
|
6
|
+
icon: layout-sidebar-reverse
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
A panel sliding in from an edge. Like [`.modal`](../modal/), the class is **pure configuration**: `drawer.css` declares nothing but `--panel-*` tokens, and [`components/panel.css`](../panel/) owns every rule.
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<div class="drawer drawer-end" id="menu" popover="auto" role="dialog" aria-labelledby="menuLabel">
|
|
13
|
+
<div class="header">
|
|
14
|
+
<p class="title" id="menuLabel">Menu</p>
|
|
15
|
+
<button class="btn-close js-dialog-close" aria-label="Close"></button>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="content">
|
|
18
|
+
<ul class="nav">...</ul>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**A position variant is required.** `.drawer` on its own pins nothing: `--drawer-start` and its siblings have no value until one of the four variants sets them.
|
|
24
|
+
|
|
25
|
+
## Position variants
|
|
26
|
+
|
|
27
|
+
| Class | Edge | Slides from |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `.drawer-start` | Inline start, left in a left-to-right document | `translateX(-100%)` |
|
|
30
|
+
| `.drawer-end` | Inline end | `translateX(100%)` |
|
|
31
|
+
| `.drawer-top` | Top | `translateY(-100%)` |
|
|
32
|
+
| `.drawer-bottom` | Bottom | `translateY(100%)` |
|
|
33
|
+
|
|
34
|
+
Each sets three things: which two insets are `0`, which two are `auto`, and the translate the panel animates from.
|
|
35
|
+
|
|
36
|
+
The start and end variants leave `--panel-inline-size: var(--drawer-width)` and let the block axis stretch. The top and bottom variants swap that, taking `--panel-block-size: var(--drawer-height)` and `--panel-inline-size: auto`.
|
|
37
|
+
|
|
38
|
+
## Why the margin stays at 0
|
|
39
|
+
|
|
40
|
+
```css
|
|
41
|
+
/* two edges pinned, the opposite ones auto */
|
|
42
|
+
--panel-inset-block: var(--drawer-top) var(--drawer-bottom);
|
|
43
|
+
--panel-inset-inline: var(--drawer-start) var(--drawer-end);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A `<dialog>` opened with `showModal()` is centred by user agent rules that only apply when the margins are `auto`. The drawer leaves `--panel-margin` at its `0` default precisely so those rules do not fire, which is what lets the same file work on a `<dialog>` and on a popover.
|
|
47
|
+
|
|
48
|
+
## It only slides, it does not fade
|
|
49
|
+
|
|
50
|
+
Unlike a modal, the drawer sets no `--panel-opacity`, so the token keeps its `1` default. The closed state is a pure translate, and the `@starting-style` block in `panel.css` replays it. That is the whole difference in the two animations.
|
|
51
|
+
|
|
52
|
+
## The case for `popover` over `<dialog>`
|
|
53
|
+
|
|
54
|
+
A drawer is the usual host for [`.panel-inline-*`](../panel/#inline-variants): a menu, a filters sidebar or a table of contents that is an overlay on mobile and a column on desktop. A `<dialog>` is exposed as `role="dialog"` even while it is styled as a column, so use `popover="auto"` and let your script set the role only while the panel is actually open.
|
|
55
|
+
|
|
56
|
+
## Sizing
|
|
57
|
+
|
|
58
|
+
`--drawer-width` and `--drawer-height` are the two dimensions. `--panel-max-inline-size` is pinned to `100%`, so a drawer wider than the viewport is clamped rather than pushed off screen.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Dropdown
|
|
4
|
+
description: A contextual menu positioned against its trigger, with a three-class transition contract.
|
|
5
|
+
weight: 4
|
|
6
|
+
icon: caret-down-square
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
A contextual menu. Unlike [`.modal`](../modal/) and [`.drawer`](../drawer/), it is not a panel: it is absolutely positioned against its trigger, stays out of the top layer, and has a tighter JavaScript contract because it animates in both directions.
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<div class="dropdown">
|
|
13
|
+
<button class="dropdown-toggle js-dropdown-toggle" aria-expanded="false">Label</button>
|
|
14
|
+
<ul class="dropdown-menu">
|
|
15
|
+
<li><a href="#">Item</a></li>
|
|
16
|
+
<li><hr></li>
|
|
17
|
+
<li><a href="#">Item</a></li>
|
|
18
|
+
</ul>
|
|
19
|
+
</div>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Three classes, three jobs
|
|
23
|
+
|
|
24
|
+
| Class | Role |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `.dropdown` | The positioning context, `inline-flex` and `position: relative` |
|
|
27
|
+
| `.dropdown-toggle` | The trigger, styled: transparent, with a chevron on `::after` |
|
|
28
|
+
| `.dropdown-menu` | The menu panel, absolutely positioned under the trigger |
|
|
29
|
+
|
|
30
|
+
The trigger needs **both** `.dropdown-toggle` for the styling and `.js-dropdown-toggle` for the script. They are separate on purpose, so a trigger can be a `.btn`, a [`.btn-share`](../../buttons/btn-share/) or a plain button and still be found by the same script.
|
|
31
|
+
|
|
32
|
+
**`.js-dropdown-toggle`'s next element sibling must be the `.dropdown-menu`.** That is how the script pairs the two; there is no `data-target` here.
|
|
33
|
+
|
|
34
|
+
## Items are styled by element, not by class
|
|
35
|
+
|
|
36
|
+
`.dropdown-menu` styles `li a` directly. There is no `.dropdown-item` rule in this package, and no `.dropdown-divider` rule either: a separator is a plain `<hr>` taking whatever [css-base](../../../css-base/base/) gives it. Adding those classes to your markup is harmless but changes nothing.
|
|
37
|
+
|
|
38
|
+
Links get `white-space: nowrap`, so the menu widens to its longest item rather than wrapping. `--dropdown-min-width` sets the floor.
|
|
39
|
+
|
|
40
|
+
## The chevron
|
|
41
|
+
|
|
42
|
+
Drawn on `::after` as a mask from `--dropdown-icon`, and rotated by the trigger's own ARIA state:
|
|
43
|
+
|
|
44
|
+
```css
|
|
45
|
+
&[aria-expanded="true"]::after { rotate: 180deg; }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
So the arrow follows `aria-expanded` rather than a state class. If your script forgets to update the attribute, the menu opens with the chevron still pointing down, which makes the omission visible during development rather than only to screen reader users.
|
|
49
|
+
|
|
50
|
+
The trigger also lays a transparent `::before` over its whole positioning context, widening the click target beyond the text.
|
|
51
|
+
|
|
52
|
+
## The three-class transition contract
|
|
53
|
+
|
|
54
|
+
The script toggles three classes on the **menu**, in this sequence:
|
|
55
|
+
|
|
56
|
+
| Class | Meaning | Effect |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| `.showing` | Opening transition has started | Visible and clickable, still at its closed opacity and offset |
|
|
59
|
+
| `.show` | Fully open | Opacity 1, translate 0 |
|
|
60
|
+
| `.hiding` | Closing transition has started | Still visible while it animates out |
|
|
61
|
+
|
|
62
|
+
```css
|
|
63
|
+
&.show, &.showing, &.hiding { pointer-events: auto; visibility: visible; }
|
|
64
|
+
&.show { opacity: 1; transform: translateY(0); }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The three exist so the menu can animate **out** before being removed from the flow. A single `.show` class would make the closing transition impossible: removing it would snap `visibility` to `hidden` on the same frame as the opacity change, and nothing would be seen.
|
|
68
|
+
|
|
69
|
+
The script must also keep `aria-expanded` on the toggle in sync with the state.
|
|
70
|
+
|
|
71
|
+
Transitions are inside `@media (prefers-reduced-motion: no-preference)`, so the three classes still work with motion disabled; they simply take effect instantly.
|
|
72
|
+
|
|
73
|
+
## Alignment
|
|
74
|
+
|
|
75
|
+
`.dropdown-menu-end` aligns the menu to the inline end of its trigger rather than the start. It is the only variant, and it goes on the menu, not on the container.
|
|
76
|
+
|
|
77
|
+
```html
|
|
78
|
+
<ul class="dropdown-menu dropdown-menu-end">...</ul>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Inside a navigation
|
|
82
|
+
|
|
83
|
+
[`.nav`](../../navigation/nav/) restyles `.dropdown-toggle` to take the navigation's own text colour, so a dropdown in a header menu matches the links beside it without a variant class.
|
|
84
|
+
|
|
85
|
+
## It uses `z-index`, not the top layer
|
|
86
|
+
|
|
87
|
+
`.dropdown-menu` is positioned with `z-index: var(--dropdown-z-index)` inside its own stacking context. Unlike a panel, it can therefore be clipped by an ancestor with `overflow: hidden` or trapped under a sticky header. If that happens, the fix is the ancestor's overflow, not a higher `z-index`.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Modal
|
|
4
|
+
description: The centred panel configuration, and how it centres and shrinks to its content at once.
|
|
5
|
+
weight: 2
|
|
6
|
+
icon: window
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
A centred panel. The class is **pure configuration**: `modal.css` declares nothing but `--panel-*` tokens, and [`components/panel.css`](../panel/) owns every rule.
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<dialog class="modal" id="contact" aria-labelledby="contactLabel">
|
|
13
|
+
<div class="header">
|
|
14
|
+
<p class="title" id="contactLabel">Contact</p>
|
|
15
|
+
<button class="btn-close js-dialog-close" aria-label="Close"></button>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="content">...</div>
|
|
18
|
+
<div class="footer">
|
|
19
|
+
<button class="btn btn-secondary js-dialog-close">Cancel</button>
|
|
20
|
+
<button class="btn" type="submit">Send</button>
|
|
21
|
+
</div>
|
|
22
|
+
</dialog>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Works on a `<dialog>` opened with `showModal()` and on any element carrying `popover="auto"`, opened with `showPopover()`. Reach for the second form only when the panel is not always modal, typically when [`.panel-inline-*`](../panel/#inline-variants) turns it into a region of the page above a breakpoint.
|
|
26
|
+
|
|
27
|
+
## Sizing
|
|
28
|
+
|
|
29
|
+
Override `--modal-max-width` on the element:
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<dialog class="modal" style="--modal-max-width: 40rem">...</dialog>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
or globally, for every modal on the site:
|
|
36
|
+
|
|
37
|
+
```css
|
|
38
|
+
@layer components {
|
|
39
|
+
.modal { --modal-max-width: 40rem; }
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`--modal-max-height`, `--modal-min-height` and `--modal-margin` are the other three sizing tokens. All `--modal-*` names are the public API; `--panel-*` is the internal one.
|
|
44
|
+
|
|
45
|
+
## Why the height is `fit-content` and not `auto`
|
|
46
|
+
|
|
47
|
+
The centring is done with insets and auto margins:
|
|
48
|
+
|
|
49
|
+
```css
|
|
50
|
+
--panel-block-size: fit-content;
|
|
51
|
+
--panel-inset-block: 0;
|
|
52
|
+
--panel-inset-inline: 0;
|
|
53
|
+
--panel-margin: auto;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
With both block insets set, an `auto` height resolves to fill the gap between them, and the auto margins then collapse to `0`. The panel would neither centre nor shrink to its content. `fit-content` is what keeps both behaviours, and it is the same reason the user agent dialog stylesheet uses it.
|
|
57
|
+
|
|
58
|
+
## Why there is no mobile breakpoint
|
|
59
|
+
|
|
60
|
+
The maximum is a `min()` rather than a media query:
|
|
61
|
+
|
|
62
|
+
```css
|
|
63
|
+
--panel-max-block-size: min(var(--modal-max-height), 100% - var(--modal-margin) * 2);
|
|
64
|
+
--panel-max-inline-size: min(var(--modal-max-width), 100% - var(--modal-margin) * 2);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
So the panel always keeps `--modal-margin` clear of the viewport edges, whatever the maximum is set to. Raising `--modal-max-width` on a narrow screen changes nothing, which is why a modal needs no responsive variant.
|
|
68
|
+
|
|
69
|
+
## Opening animation
|
|
70
|
+
|
|
71
|
+
`--panel-opacity: 0` and `--panel-translate: var(--modal-translate)` describe the **closed** state, so a modal both fades and moves on the way in. A drawer leaves the opacity alone and only slides. Both are replayed by the `@starting-style` block in `panel.css`, and both are skipped under `prefers-reduced-motion: reduce`.
|
|
72
|
+
|
|
73
|
+
## Accessibility
|
|
74
|
+
|
|
75
|
+
Point `aria-labelledby` at the `id` of the title in the header. A `<dialog>` opened with `showModal()` gets the focus trap, Escape and inertness from the browser; the `popover` path gets light dismiss and Escape but no role, which your script has to supply.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Panel
|
|
4
|
+
description: The shared skeleton behind .modal and .drawer, the two opening strategies, the inline variants and the backdrop.
|
|
5
|
+
weight: 1
|
|
6
|
+
icon: layout-sidebar
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
`components/panel.css` is the largest file in the package and the only one with no class of its own. It owns every rule behind [`.modal`](../modal/) and [`.drawer`](../drawer/), which contribute nothing but `--panel-*` values.
|
|
10
|
+
|
|
11
|
+
There is **no `.panel` class.** The selectors read `:where(.modal, .drawer)`, so the skeleton arrives with either variant class.
|
|
12
|
+
|
|
13
|
+
## Structure
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<dialog class="modal" id="contact" aria-labelledby="contactLabel">
|
|
17
|
+
<div class="header">
|
|
18
|
+
<p class="title" id="contactLabel">Contact</p>
|
|
19
|
+
<button class="btn-close js-dialog-close" aria-label="Close"></button>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="content">...</div>
|
|
22
|
+
<div class="footer">...</div>
|
|
23
|
+
</dialog>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
All three sections are optional, and all three are matched as **direct children** (`& > .header`).
|
|
27
|
+
|
|
28
|
+
| Section | Behaviour |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `.header` | Fixed height, title and close button pushed apart, bottom border |
|
|
31
|
+
| `.content` | Takes the remaining height and scrolls, with `overscroll-behavior: contain` |
|
|
32
|
+
| `.footer` | Fixed height, top border, stacked on mobile and right-aligned from `--sm` up |
|
|
33
|
+
|
|
34
|
+
The panel itself carries **no padding**. It lives on the three sections instead, so that a click on the panel element is always a click on the backdrop rather than on a transparent margin.
|
|
35
|
+
|
|
36
|
+
## Two ways to open a panel
|
|
37
|
+
|
|
38
|
+
**The tag picks the strategy, not the class.** Either variant works both ways.
|
|
39
|
+
|
|
40
|
+
### `<dialog>` with `showModal()`
|
|
41
|
+
|
|
42
|
+
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.
|
|
43
|
+
|
|
44
|
+
This is the right choice when the panel is **always** a dialog.
|
|
45
|
+
|
|
46
|
+
### `popover="auto"` with `showPopover()`
|
|
47
|
+
|
|
48
|
+
Same top layer, same native `::backdrop`, but the element carries no implicit role.
|
|
49
|
+
|
|
50
|
+
Use this when the panel becomes a static region of the page above a breakpoint, which is what the [inline variants](#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.
|
|
51
|
+
|
|
52
|
+
## How open and closed are expressed
|
|
53
|
+
|
|
54
|
+
`display` is only ever declared on a state selector, never on the panel itself:
|
|
55
|
+
|
|
56
|
+
```css
|
|
57
|
+
:where(.modal, .drawer):not(:where([open], :popover-open)) { display: none; }
|
|
58
|
+
:where(.modal, .drawer):is([open], :popover-open) { display: flex; opacity: 1; transform: none; }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Declaring `display` on the panel would beat the user agent rule that hides a closed `<dialog>`, and a closed dialog would render.
|
|
62
|
+
|
|
63
|
+
The two selectors differ deliberately. The closed rule uses `:where()` throughout and carries **zero specificity**. The open rule uses `:is()` on the state, giving it 0,1,0, which is what lets it beat the unstyled base rule that comes later in the file on specificity rather than on source order, while still leaving a host component such as `.toc .drawer` (0,2,0) room to override without escalating.
|
|
64
|
+
|
|
65
|
+
Everything else about the closed state lives in tokens: `--panel-opacity` and `--panel-translate` are the values the panel animates **from**, which is why a drawer only slides while a modal also fades.
|
|
66
|
+
|
|
67
|
+
The entry transition needs those values spelled out a second time, because opening is a discrete `display` change:
|
|
68
|
+
|
|
69
|
+
```css
|
|
70
|
+
@starting-style {
|
|
71
|
+
:where(.modal, .drawer):is([open], :popover-open) {
|
|
72
|
+
opacity: var(--panel-opacity, 1);
|
|
73
|
+
transform: var(--panel-translate, none);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`transition-behavior: allow-discrete` keeps the panel displayed while it transitions out, and keeps a `<dialog>` in the top layer until the transition ends.
|
|
79
|
+
|
|
80
|
+
## The backdrop
|
|
81
|
+
|
|
82
|
+
Both opening paths put the panel in the top layer, so both get the native `::backdrop`, which inherits from the panel and therefore reads the same `--panel-*` tokens. **No backdrop node is ever injected, and no `z-index` is involved.**
|
|
83
|
+
|
|
84
|
+
This is the reason the class-driven path uses `popover` rather than a plain element. A backdrop placed outside the panel sits in the root stacking context while the panel stays confined to its nearest stacking ancestor, a sticky sidebar wrapper or a sticky header, so the veil paints over the panel and no `z-index` can lift it out. Placing the backdrop next to the panel only moves the problem: it then inherits the same confinement, and anything painted later shows through it. The top layer removes the question entirely, which is exactly what it exists for.
|
|
85
|
+
|
|
86
|
+
Page scroll is locked from the panel's own state, with no class on `<body>` to manage:
|
|
87
|
+
|
|
88
|
+
```css
|
|
89
|
+
body:has(:where(.modal, .drawer):where([open], :popover-open)) { overflow: hidden; }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Inline variants
|
|
93
|
+
|
|
94
|
+
`.panel-inline-*` turns a panel into an ordinary region of the page from a breakpoint upwards, while it stays an overlay below.
|
|
95
|
+
|
|
96
|
+
| Class | Becomes inline at |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `.panel-inline-sm` | `--sm`, 768px |
|
|
99
|
+
| `.panel-inline-md` | `--md`, 1024px |
|
|
100
|
+
| `.panel-inline-lg` | `--lg`, 1440px |
|
|
101
|
+
| `.panel-inline-xl` | `--xl`, 1600px |
|
|
102
|
+
|
|
103
|
+
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. **The panel must not be a `<dialog>` when one of these is used.**
|
|
104
|
+
|
|
105
|
+
Above the breakpoint the variant neutralises the whole panel rule: position, insets, size, shadow, overflow, transition, and it hides `.header` and `.footer` while flattening `.content`.
|
|
106
|
+
|
|
107
|
+
### Colours go too, not just geometry
|
|
108
|
+
|
|
109
|
+
A region of the page carries none of the panel's chrome, so the variant hands five custom properties back to the parent:
|
|
110
|
+
|
|
111
|
+
```css
|
|
112
|
+
--color-link: inherit;
|
|
113
|
+
--color-link-active: inherit;
|
|
114
|
+
--color-link-hover: inherit;
|
|
115
|
+
--nav-color-text: inherit;
|
|
116
|
+
--nav-color-text-active: inherit;
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`inherit` on a custom property is what hands it back, and it has to be done token by token. Resetting the `--panel-color-text-*` tokens they derive from would make them invalid at computed-value time rather than page-valued, and every link would fall back to the surrounding text colour instead of the site's link colour.
|
|
120
|
+
|
|
121
|
+
If you add a property to the panel rule, neutralise it in all four variants. That is the maintenance cost of the pattern, and the reason the block is written once per breakpoint rather than derived.
|
|
122
|
+
|
|
123
|
+
## Hiding the trigger
|
|
124
|
+
|
|
125
|
+
`.panel-trigger-*` is the companion, applied to the **trigger button** rather than to the panel. It hides the trigger at the breakpoint where the panel stops being an overlay.
|
|
126
|
+
|
|
127
|
+
| Class | Trigger hidden from |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `.panel-trigger-sm` | `--sm`, 768px |
|
|
130
|
+
| `.panel-trigger-md` | `--md`, 1024px |
|
|
131
|
+
| `.panel-trigger-lg` | `--lg`, 1440px |
|
|
132
|
+
| `.panel-trigger-xl` | `--xl`, 1600px |
|
|
133
|
+
|
|
134
|
+
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.
|
|
135
|
+
|
|
136
|
+
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.
|
|
137
|
+
|
|
138
|
+
## The JavaScript contract
|
|
139
|
+
|
|
140
|
+
This package ships no JavaScript. `.modal` and `.drawer` expect a script that provides:
|
|
141
|
+
|
|
142
|
+
| Hook | Role |
|
|
143
|
+
| --- | --- |
|
|
144
|
+
| `.js-dialog-toggle[data-target="#id"]` | Opens the panel it points at |
|
|
145
|
+
| `.js-dialog-close` | Any element inside the panel that closes it |
|
|
146
|
+
|
|
147
|
+
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.
|
|
148
|
+
|
|
149
|
+
One case CSS cannot cover: a panel carrying a `.panel-inline-*` class that is **open** when the viewport crosses the breakpoint. The script has to close it, because nothing in CSS releases the backdrop, the scroll lock and the inert background.
|
|
150
|
+
|
|
151
|
+
## Source order, not specificity
|
|
152
|
+
|
|
153
|
+
Most of this file is wrapped in `:where()` and carries no specificity at all, which is what lets `.modal`, `.drawer` and your own overrides win without escalating:
|
|
154
|
+
|
|
155
|
+
```css
|
|
156
|
+
.modal { border-radius: 0; } /* enough, no !important needed */
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The `.panel-inline-*` variants are the exception. They are plain class selectors that must beat `.modal` and `.drawer`, and they do so on **source order**. Alphabetical import order already places `panel.css` after `drawer.css` and `modal.css`; keep it that way if you import files individually. See [Cascade layers](../../cascade-layers/).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Utilities
|
|
4
|
+
description: The one utility this package ships, and why it is a utility rather than a component rule.
|
|
5
|
+
weight: 8
|
|
6
|
+
icon: tools
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
One utility, in its own layer.
|
|
10
|
+
|
|
11
|
+
| Page | Class | Purpose |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| [Scrollsnap](scrollsnap/) | `.scrollsnap` | Turns a grid into a horizontal snap carousel |
|
|
14
|
+
|
|
15
|
+
## Why it is not a component
|
|
16
|
+
|
|
17
|
+
`.scrollsnap` changes the `grid-template-columns` that a component such as [`.items`](../content/items/) declares for itself in `@layer components`. A later layer beats an earlier one regardless of specificity, so putting it in `@layer utilities` is what lets a single class opt an existing component into carousel behaviour without rewriting the component.
|
|
18
|
+
|
|
19
|
+
That is also the test for anything else that might land here: a utility is a **modifier applied to something else**, not a thing of its own. If it has its own markup, it is a component.
|
|
20
|
+
|
|
21
|
+
`@layer utilities` must come last in your layer declaration for this to hold. See [Cascade layers](../cascade-layers/).
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
isIndex: false
|
|
3
|
-
title:
|
|
4
|
-
description:
|
|
5
|
-
weight:
|
|
6
|
-
icon:
|
|
3
|
+
title: Scrollsnap
|
|
4
|
+
description: Turns a grid into a horizontal snap carousel below a breakpoint, bleeding into the gutter so the next item peeks in.
|
|
5
|
+
weight: 1
|
|
6
|
+
icon: arrow-left-right
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
## `.scrollsnap`
|
|
10
|
-
|
|
11
9
|
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
10
|
|
|
13
11
|
```html
|
|
@@ -34,9 +32,22 @@ This is the one thing to get right, and it reads backwards from most responsive
|
|
|
34
32
|
|
|
35
33
|
**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
34
|
|
|
37
|
-
##
|
|
35
|
+
## How a variant hands the grid back
|
|
36
|
+
|
|
37
|
+
Above its rung, every property is set to `revert-layer` rather than to a second set of values:
|
|
38
|
+
|
|
39
|
+
```css
|
|
40
|
+
@media (--md) {
|
|
41
|
+
.scrollsnap-md {
|
|
42
|
+
grid-auto-flow: revert-layer;
|
|
43
|
+
grid-template-columns: revert-layer;
|
|
44
|
+
margin-inline: revert-layer;
|
|
45
|
+
/* … */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
38
49
|
|
|
39
|
-
|
|
50
|
+
`revert-layer` hands the property back to the layer below, which is the component's own grid rule in `@layer components`. So there is no duplicated set of grid values to keep in step: whatever `.items` declares is what comes back, including any override you wrote yourself.
|
|
40
51
|
|
|
41
52
|
## Item width
|
|
42
53
|
|
|
@@ -51,7 +62,7 @@ Items take `--items-min-width`, the same value that drives the grid, capped by `
|
|
|
51
62
|
|
|
52
63
|
## The bleed, and why margin and padding disagree
|
|
53
64
|
|
|
54
|
-
The row pulls out to the screen edge using `--container-bleed`, published by `.container` in [css-base](
|
|
65
|
+
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
66
|
|
|
56
67
|
Outside a container there is no published value, and the two `var()` fallbacks differ **on purpose**, because margin and padding answer different questions:
|
|
57
68
|
|
|
@@ -65,3 +76,9 @@ Inside a container the two collapse to one value: both read the published bleed,
|
|
|
65
76
|
`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
77
|
|
|
67
78
|
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.
|
|
79
|
+
|
|
80
|
+
## It needs a build step
|
|
81
|
+
|
|
82
|
+
The four responsive variants are written with `@media (--sm)` and its siblings, which rely on the `@custom-media` rules declared in css-base. No browser implements `@custom-media`, so without [postcss-custom-media](https://www.npmjs.com/package/postcss-custom-media) every `.scrollsnap-*` variant becomes a permanent carousel: the base block applies and the block that hands the grid back is dropped.
|
|
83
|
+
|
|
84
|
+
The failure is silent. See [the package index](../../).
|
package/package.json
CHANGED
package/docs/buttons.md
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
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.
|