@uncinq/css-components 1.26.0 → 1.27.0
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/css/components/btn-close.css +7 -0
- package/css/components/btn-menu.css +7 -0
- package/css/components/drawer.css +4 -2
- package/css/components/dropdown.css +42 -0
- package/css/components/modal.css +4 -2
- package/css/components/panel.css +16 -10
- package/css/utilities/scrollsnap.css +6 -1
- package/docs/content/items.md +1 -1
- package/docs/overlays/dropdown.md +8 -0
- package/docs/overlays/overview.md +1 -1
- package/docs/overlays/panel.md +21 -14
- package/docs/utilities/scrollsnap.md +21 -1
- package/package.json +1 -1
|
@@ -29,6 +29,13 @@
|
|
|
29
29
|
width: var(--btn-close-icon-size, var(--icon-size));
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/* Not a .btn, so the focus ring is restated: the browser's own one can
|
|
33
|
+
vanish on a coloured panel */
|
|
34
|
+
&:focus-visible {
|
|
35
|
+
outline: var(--focus-outline-width) var(--focus-outline-style) var(--focus-color-outline);
|
|
36
|
+
outline-offset: var(--focus-outline-offset, 2px);
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
@media (hover: hover) and (pointer: fine) {
|
|
33
40
|
&:hover {
|
|
34
41
|
background-color: var(--btn-close-color-background-hover, var(--btn-control-color-background-hover));
|
|
@@ -30,6 +30,13 @@
|
|
|
30
30
|
width: var(--btn-menu-icon-size, var(--icon-size));
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/* Not a .btn, so the focus ring is restated: the browser's own one can
|
|
34
|
+
vanish on a coloured header */
|
|
35
|
+
&:focus-visible {
|
|
36
|
+
outline: var(--focus-outline-width) var(--focus-outline-style) var(--focus-color-outline);
|
|
37
|
+
outline-offset: var(--focus-outline-offset, 2px);
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
@media (hover: hover) and (pointer: fine) {
|
|
34
41
|
&:hover {
|
|
35
42
|
background-color: var(--btn-menu-color-background-hover, var(--btn-control-color-background-hover));
|
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
* Position variants: .drawer-start | .drawer-end | .drawer-top | .drawer-bottom
|
|
17
17
|
* Inline variants: .panel-inline-* — see components/panel.css
|
|
18
18
|
*
|
|
19
|
-
* Triggers:
|
|
20
|
-
*
|
|
19
|
+
* Triggers: popovertarget="id", or commandfor="id" command="show-modal" for a
|
|
20
|
+
* <dialog> — native, no script needed; see components/panel.css
|
|
21
|
+
* Close: the same with popovertargetaction="hide" or command="close",
|
|
22
|
+
* Escape, or a click outside
|
|
21
23
|
*/
|
|
22
24
|
@layer components {
|
|
23
25
|
.drawer {
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Trigger: .js-dropdown-toggle (nextElementSibling must be .dropdown-menu)
|
|
12
12
|
*
|
|
13
|
+
* Without JavaScript (scripting: none), the menu opens on hover and on
|
|
14
|
+
* keyboard focus within the .dropdown instead, so its links stay reachable.
|
|
15
|
+
*
|
|
13
16
|
* Expected HTML:
|
|
14
17
|
* <div class="dropdown">
|
|
15
18
|
* <button class="js-dropdown-toggle" aria-expanded="false">Label</button>
|
|
@@ -130,6 +133,45 @@
|
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
|
|
136
|
+
/* ── Without JavaScript ──────────────────────────────────────────────
|
|
137
|
+
* No script to toggle .show: the menu follows the pointer and the keyboard
|
|
138
|
+
* focus instead. Scoped to scripting: none, so it never fights the script's
|
|
139
|
+
* own state when there is one. */
|
|
140
|
+
|
|
141
|
+
@media (scripting: none) {
|
|
142
|
+
/* The toggle opens nothing by itself here: it stops covering its item,
|
|
143
|
+
so a link next to it stays clickable */
|
|
144
|
+
.dropdown-toggle::before {
|
|
145
|
+
content: none;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Closing waits, so the pointer can cross a gap between the item and
|
|
149
|
+
its menu — a mega menu below a header, for instance. The menu has to
|
|
150
|
+
take the pointer meanwhile: once closed, visibility: hidden keeps it
|
|
151
|
+
out of the way. */
|
|
152
|
+
.dropdown > .dropdown-menu {
|
|
153
|
+
pointer-events: auto;
|
|
154
|
+
transition-delay: 0.4s;
|
|
155
|
+
transition-property: opacity, transform, visibility;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Keyboard focus only: a click leaves focus on the toggle, and the menu
|
|
159
|
+
would stay open once the pointer has gone */
|
|
160
|
+
.dropdown:is(:hover, :has(:focus-visible)) {
|
|
161
|
+
> .dropdown-menu {
|
|
162
|
+
opacity: 1;
|
|
163
|
+
pointer-events: auto;
|
|
164
|
+
transform: none;
|
|
165
|
+
transition-delay: 0s;
|
|
166
|
+
visibility: visible;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
> .dropdown-toggle::after {
|
|
170
|
+
rotate: 180deg;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
133
175
|
/* ── Alignment variants ──────────────────────────────────────────── */
|
|
134
176
|
|
|
135
177
|
.dropdown-menu-end {
|
package/css/components/modal.css
CHANGED
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Per-instance sizing: override --modal-max-width on the element.
|
|
16
16
|
*
|
|
17
|
-
* Triggers:
|
|
18
|
-
*
|
|
17
|
+
* Triggers: popovertarget="id", or commandfor="id" command="show-modal" for a
|
|
18
|
+
* <dialog> — native, no script needed; see components/panel.css
|
|
19
|
+
* Close: the same with popovertargetaction="hide" or command="close",
|
|
20
|
+
* Escape, or a click outside
|
|
19
21
|
*/
|
|
20
22
|
@layer components {
|
|
21
23
|
.modal {
|
package/css/components/panel.css
CHANGED
|
@@ -7,14 +7,19 @@
|
|
|
7
7
|
* drawer.css sticks them to an edge.
|
|
8
8
|
*
|
|
9
9
|
* The tag picks the strategy, not the class. Either variant can be:
|
|
10
|
-
* - <dialog> opened
|
|
11
|
-
*
|
|
12
|
-
* - any element carrying popover="auto"
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
10
|
+
* - <dialog> opened as a modal — [open], native ::backdrop, top layer, and the
|
|
11
|
+
* browser handles Escape, the focus trap and inertness;
|
|
12
|
+
* - any element carrying popover="auto" — same top layer, same ::backdrop,
|
|
13
|
+
* but no role of its own. Use this when the panel becomes a static region
|
|
14
|
+
* of the page above a breakpoint (see .panel-inline-*): a <dialog> is
|
|
15
|
+
* exposed as role="dialog" even when it is only styled as a sidebar,
|
|
16
|
+
* whereas a script can set that role solely while the panel is open.
|
|
17
|
+
*
|
|
18
|
+
* Both open without a script, from native invokers on the buttons:
|
|
19
|
+
* popovertarget="x" for a popover, commandfor="x" command="show-modal" for a
|
|
20
|
+
* <dialog>, and their closing forms, popovertargetaction="hide" and
|
|
21
|
+
* command="close". closedby="any" lets a click on a <dialog>'s backdrop close
|
|
22
|
+
* it too. showModal() and showPopover() remain available to a script.
|
|
18
23
|
*
|
|
19
24
|
* Most selectors here are :where()-wrapped so they carry no specificity: the
|
|
20
25
|
* variants and the host components always win without escalating. The
|
|
@@ -27,10 +32,11 @@
|
|
|
27
32
|
* namespace, so --modal-* and --drawer-* stay the public API.
|
|
28
33
|
*
|
|
29
34
|
* Expected HTML:
|
|
30
|
-
* <
|
|
35
|
+
* <button commandfor="x" command="show-modal">...</button>
|
|
36
|
+
* <dialog class="modal" id="x" aria-labelledby="xLabel" closedby="any">
|
|
31
37
|
* <div class="header">
|
|
32
38
|
* <p class="title" id="xLabel">...</p>
|
|
33
|
-
* <button class="btn-close
|
|
39
|
+
* <button class="btn-close" commandfor="x" command="close"></button>
|
|
34
40
|
* </div>
|
|
35
41
|
* <div class="content">...</div>
|
|
36
42
|
* <div class="footer">...</div>
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Item width: --items-min-width (or --max-width-item), capped by
|
|
22
22
|
* --items-carousel-max-width so the next item always peeks in.
|
|
23
|
+
* --items-carousel-min-width, unset by default, overrides the first for every
|
|
24
|
+
* carousel at once: set on :root, it gives them all one width whatever their
|
|
25
|
+
* grid says, and a component re-declaring it on itself keeps its own. It is
|
|
26
|
+
* only read while the row scrolls, so the grid it turns back into keeps
|
|
27
|
+
* --items-min-width.
|
|
23
28
|
*
|
|
24
29
|
* Bleed: --container-bleed, published by .container in @uncinq/css-base — one
|
|
25
30
|
* gutter while the container spans the viewport, 0 once it caps, so the row
|
|
@@ -55,7 +60,7 @@
|
|
|
55
60
|
.scrollsnap-md,
|
|
56
61
|
.scrollsnap-lg,
|
|
57
62
|
.scrollsnap-xl {
|
|
58
|
-
grid-auto-columns: min(var(--items-min-width, var(--max-width-item)), var(--items-carousel-max-width, 75vw));
|
|
63
|
+
grid-auto-columns: min(var(--items-carousel-min-width, var(--items-min-width, var(--max-width-item))), var(--items-carousel-max-width, 75vw));
|
|
59
64
|
grid-auto-flow: column;
|
|
60
65
|
grid-template-columns: none;
|
|
61
66
|
justify-content: start;
|
package/docs/content/items.md
CHANGED
|
@@ -73,4 +73,4 @@ Add a [`.scrollsnap-*`](../../utilities/scrollsnap/) class to the same element.
|
|
|
73
73
|
<div class="items scrollsnap-md">...</div>
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
The carousel reads the same `--items-min-width`, so item width stays consistent between the two modes.
|
|
76
|
+
The carousel reads the same `--items-min-width`, so item width stays consistent between the two modes, unless `--items-carousel-min-width` gives the carousels a width of their own (see [Scrollsnap](../../utilities/scrollsnap/#one-width-for-every-carousel)).
|
|
@@ -70,6 +70,14 @@ The script must also keep `aria-expanded` on the toggle in sync with the state.
|
|
|
70
70
|
|
|
71
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
72
|
|
|
73
|
+
## Without JavaScript
|
|
74
|
+
|
|
75
|
+
With scripting disabled, nothing toggles `.show`, so under `@media (scripting: none)` the menu opens on `:hover` of the `.dropdown` and on keyboard focus inside it (`:has(:focus-visible)`) instead, and the chevron turns with it. The links stay reachable with a pointer and with the keyboard. Keyboard focus rather than `:focus-within`: a click leaves the focus on the toggle, and the menu would stay open after the pointer has gone.
|
|
76
|
+
|
|
77
|
+
Two more adjustments apply in that mode. The toggle's `::before` no longer covers the whole item, since the toggle opens nothing by itself: a link next to it stays clickable. And the menu waits 0.4s before closing, so the pointer can cross a gap between the item and its menu, as with a mega menu laid out below a header.
|
|
78
|
+
|
|
79
|
+
The rule never applies when a script runs, so it cannot fight the script's own state. It reaches the menu as a **direct child** of `.dropdown`, like the rest of the contract.
|
|
80
|
+
|
|
73
81
|
## Alignment
|
|
74
82
|
|
|
75
83
|
`.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.
|
|
@@ -31,4 +31,4 @@ Either variant works both ways, and the choice is an accessibility one rather th
|
|
|
31
31
|
|
|
32
32
|
## No JavaScript ships here
|
|
33
33
|
|
|
34
|
-
`.modal
|
|
34
|
+
`.modal` and `.drawer` open and close without one, from native invokers on their buttons; a script only adds the dialog role, the inert background and the cases the platform leaves out. `.dropdown` expects a script, and falls back to hover and focus when scripting is off. Each page documents the contract it needs.
|
package/docs/overlays/panel.md
CHANGED
|
@@ -13,10 +13,11 @@ There is **no `.panel` class.** The selectors read `:where(.modal, .drawer)`, so
|
|
|
13
13
|
## Structure
|
|
14
14
|
|
|
15
15
|
```html
|
|
16
|
-
<
|
|
16
|
+
<button commandfor="contact" command="show-modal">Contact</button>
|
|
17
|
+
<dialog class="modal" id="contact" aria-labelledby="contactLabel" closedby="any">
|
|
17
18
|
<div class="header">
|
|
18
19
|
<p class="title" id="contactLabel">Contact</p>
|
|
19
|
-
<button class="btn-close
|
|
20
|
+
<button class="btn-close" commandfor="contact" command="close" aria-label="Close"></button>
|
|
20
21
|
</div>
|
|
21
22
|
<div class="content">...</div>
|
|
22
23
|
<div class="footer">...</div>
|
|
@@ -35,17 +36,17 @@ The panel itself carries **no padding**. It lives on the three sections instead,
|
|
|
35
36
|
|
|
36
37
|
## Two ways to open a panel
|
|
37
38
|
|
|
38
|
-
**The tag picks the strategy, not the class.** Either variant works both ways.
|
|
39
|
+
**The tag picks the strategy, not the class.** Either variant works both ways, and both open **without JavaScript**, from a native invoker on the button. See [The JavaScript contract](#the-javascript-contract).
|
|
39
40
|
|
|
40
|
-
### `<dialog
|
|
41
|
+
### `<dialog>`, opened as a modal
|
|
41
42
|
|
|
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
|
+
Opened by `command="show-modal"`, or `showModal()` from a script. 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, and `closedby="any"` adds the click on the backdrop.
|
|
43
44
|
|
|
44
45
|
This is the right choice when the panel is **always** a dialog.
|
|
45
46
|
|
|
46
|
-
### `popover="auto"`
|
|
47
|
+
### `popover="auto"`
|
|
47
48
|
|
|
48
|
-
Same top layer, same native `::backdrop`, but the element carries no implicit role.
|
|
49
|
+
Opened by `popovertarget`, or `showPopover()` from a script. Same top layer, same native `::backdrop`, but the element carries no implicit role.
|
|
49
50
|
|
|
50
51
|
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
|
|
|
@@ -133,18 +134,24 @@ If you add a property to the panel rule, neutralise it in all four variants. Tha
|
|
|
133
134
|
|
|
134
135
|
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
|
|
|
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
|
+
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 an attribute — `popovertarget`, `commandfor` or a script's `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
|
|
|
138
139
|
## The JavaScript contract
|
|
139
140
|
|
|
140
|
-
This package ships no JavaScript
|
|
141
|
+
This package ships no JavaScript, and a panel does not strictly need any: the browser opens and closes it by itself, from native invokers on the buttons.
|
|
141
142
|
|
|
142
|
-
|
|
|
143
|
-
| --- | --- |
|
|
144
|
-
|
|
|
145
|
-
|
|
|
143
|
+
| Panel | Trigger | Close button |
|
|
144
|
+
| --- | --- | --- |
|
|
145
|
+
| `popover="auto"` | `popovertarget="id"` | `popovertarget="id" popovertargetaction="hide"` |
|
|
146
|
+
| `<dialog>` | `commandfor="id" command="show-modal"` | `commandfor="id" command="close"` |
|
|
147
|
+
|
|
148
|
+
Escape works on both. A click outside closes a popover through light dismiss, and a `<dialog>` once it carries `closedby="any"`. `popovertarget` is Baseline 2024, invoker commands (`commandfor`) Baseline 2025.
|
|
149
|
+
|
|
150
|
+
A script then adds what the platform does not:
|
|
146
151
|
|
|
147
|
-
|
|
152
|
+
- `role="dialog"` and `aria-modal` on a popover, only while it is open, and an inert background behind it;
|
|
153
|
+
- closing the panel when one of its in-page anchors is followed;
|
|
154
|
+
- opening and closing for markup without native invokers, or for a browser that does not know invoker commands yet, through hooks of its choosing — for instance `.js-dialog-toggle[data-target="#id"]` and `.js-dialog-close`.
|
|
148
155
|
|
|
149
156
|
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
157
|
|
|
@@ -53,13 +53,33 @@ Above its rung, every property is set to `revert-layer` rather than to a second
|
|
|
53
53
|
|
|
54
54
|
```css
|
|
55
55
|
grid-auto-columns: min(
|
|
56
|
-
var(--items-min-width, var(--max-width-item)),
|
|
56
|
+
var(--items-carousel-min-width, var(--items-min-width, var(--max-width-item))),
|
|
57
57
|
var(--items-carousel-max-width, 75vw)
|
|
58
58
|
);
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
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.
|
|
62
62
|
|
|
63
|
+
### One width for every carousel
|
|
64
|
+
|
|
65
|
+
`--items-carousel-min-width` is unset by default, so each carousel keeps the width its own grid gives it. Set it to give every carousel the same width, whatever their grid says:
|
|
66
|
+
|
|
67
|
+
```css
|
|
68
|
+
@layer tokens {
|
|
69
|
+
:root {
|
|
70
|
+
--items-carousel-min-width: 25ch;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
It inherits, so it can be set on a section rather than on `:root` to cover only the carousels inside it. A component that has to keep its own width hands it back on itself, which wins over the inherited value:
|
|
76
|
+
|
|
77
|
+
```css
|
|
78
|
+
.logos { --items-carousel-min-width: var(--items-min-width); }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
It is only read while the row scrolls. Above its rung the grid comes back with `--items-min-width`, so the carousels can be narrowed without touching the grids they turn back into.
|
|
82
|
+
|
|
63
83
|
## The bleed, and why margin and padding disagree
|
|
64
84
|
|
|
65
85
|
The row pulls out to the screen edge using `--container-bleed`, published by `.container` in [css-base](../../../css-base/layouts/container/#the---container-bleed-contract). The property inherits, so it reaches the row from a `.container` however far up it sits.
|