@uncinq/css-components 1.16.2 → 1.16.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -10
- package/css/components/alert.css +5 -0
- package/css/components/panel.css +8 -6
- package/docs/_index.md +0 -84
- package/docs/buttons/_index.md +7 -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/buttons/overview.md +75 -0
- package/docs/cascade-layers.md +9 -7
- package/docs/content/_index.md +7 -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/overview.md +39 -0
- package/docs/content/surtitle.md +43 -0
- package/docs/embeds/_index.md +7 -0
- package/docs/embeds/embed.md +53 -0
- package/docs/embeds/map.md +57 -0
- package/docs/embeds/overview.md +25 -0
- package/docs/embeds/video.md +53 -0
- package/docs/forms/_index.md +7 -0
- package/docs/forms/form-check.md +77 -0
- package/docs/forms/form.md +67 -0
- package/docs/forms/overview.md +31 -0
- package/docs/navigation/_index.md +7 -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/overview.md +29 -0
- package/docs/navigation/pagination.md +77 -0
- package/docs/overlays/_index.md +7 -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/overview.md +34 -0
- package/docs/overlays/panel.md +159 -0
- package/docs/overview.md +93 -0
- package/docs/utilities/_index.md +7 -0
- package/docs/utilities/overview.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,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Embed
|
|
4
|
+
description: A responsive container for third-party embeds, holding an aspect ratio so the page does not reflow.
|
|
5
|
+
weight: 2
|
|
6
|
+
icon: aspect-ratio
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
A responsive container for third-party embeds, holding an aspect ratio so the page does not reflow once the iframe loads.
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<div class="embed">
|
|
13
|
+
<iframe src="https://..." title="Video title" allowfullscreen></iframe>
|
|
14
|
+
</div>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Accepts `iframe`, `video`, `embed` or `object` as its child, each stretched to fill the box.
|
|
18
|
+
|
|
19
|
+
## The ratio
|
|
20
|
+
|
|
21
|
+
`--embed-ratio` defaults to the video ratio token. Override it per instance for a square or portrait embed:
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<div class="embed" style="--embed-ratio: 1">...</div>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
or per context, which is usually better:
|
|
28
|
+
|
|
29
|
+
```css
|
|
30
|
+
.podcast-player { --embed-ratio: 4 / 1; }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Frame and shadow
|
|
34
|
+
|
|
35
|
+
`--embed-border`, `--embed-border-radius` and `--embed-box-shadow` are the three decoration tokens. `overflow: hidden` is what lets the radius clip the iframe at the corners, since an iframe cannot be given one of its own.
|
|
36
|
+
|
|
37
|
+
Note that `--embed-border` is a **shorthand** token taking width, style and colour together, unlike most components in this package which split the three. Set it as one value:
|
|
38
|
+
|
|
39
|
+
```css
|
|
40
|
+
.embed { --embed-border: 1px solid var(--color-border); }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Always give the iframe a title
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<iframe src="..." title="2024 annual report, presented by the CFO"></iframe>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
It is the only accessible name an embedded frame has. Without it a screen reader announces "frame" and the reader has to enter it to find out what it holds.
|
|
50
|
+
|
|
51
|
+
## Embed or video?
|
|
52
|
+
|
|
53
|
+
Use `.embed` for a **third-party player**, where the content is an iframe you do not control. Use [`.video`](../video/) for a native `<video>` element, which needs a positioning context for its pause control rather than a ratio wrapper.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Map
|
|
4
|
+
description: A Leaflet map container, and where Leaflet's own stylesheet belongs in the layer order.
|
|
5
|
+
weight: 4
|
|
6
|
+
icon: map
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
A map container, driven by Leaflet through a `js-map` hook.
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<div class="map js-map"
|
|
13
|
+
data-markers="[...]"
|
|
14
|
+
data-zoom="12"
|
|
15
|
+
data-tile="3"
|
|
16
|
+
data-marker-hidden="false"></div>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
| Attribute | Role |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `data-markers` | The markers to plot |
|
|
22
|
+
| `data-zoom` | Initial zoom level |
|
|
23
|
+
| `data-tile` | Tile layer selection |
|
|
24
|
+
| `data-marker-hidden` | Whether markers start hidden |
|
|
25
|
+
|
|
26
|
+
This package provides the container only: ratio, background, radius and margins. **Leaflet itself, its stylesheet and the script reading those attributes are yours to supply.**
|
|
27
|
+
|
|
28
|
+
## It breaks out on mobile
|
|
29
|
+
|
|
30
|
+
```css
|
|
31
|
+
@media not (--sm) {
|
|
32
|
+
--map-ratio: var(--map-ratio-mobile);
|
|
33
|
+
|
|
34
|
+
.container & { margin-inline: calc(-1 * var(--gutter)); }
|
|
35
|
+
.row { display: block; }
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Three changes below 768px. The ratio switches to `--map-ratio-mobile`, usually taller, because a wide map on a narrow screen shows almost nothing useful. Inside a `.container` the map pulls out to the screen edges by one gutter. And a `.row` inside it stops laying out in columns.
|
|
40
|
+
|
|
41
|
+
The bleed is deliberately scoped to `.container &`, so a map in a card or a sidebar stays inside its parent instead of overflowing something with no room to give.
|
|
42
|
+
|
|
43
|
+
## Where Leaflet's CSS goes
|
|
44
|
+
|
|
45
|
+
This is the clearest case in the package for why the layer order matters. Leaflet ships its own stylesheet, and a map is often loaded lazily.
|
|
46
|
+
|
|
47
|
+
```css
|
|
48
|
+
@layer reset, tokens, libs, base, vendors, layouts, components, pages, utilities;
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Import Leaflet's stylesheet into `@layer libs`, and any adjustments you write into `@layer vendors`. A layer's position is fixed the first time its name appears, so a stylesheet injected at runtime lands in the slot its layer already occupies rather than at the end of the cascade. Without that, a lazily loaded Leaflet outranks your own rules purely on source order, and the map's marker styling starts winning arguments it should lose.
|
|
52
|
+
|
|
53
|
+
See [Cascade layers](../../cascade-layers/).
|
|
54
|
+
|
|
55
|
+
## Accessibility
|
|
56
|
+
|
|
57
|
+
A map with no text alternative is unusable without sight. Whatever the map shows, addresses, opening hours, a route, should also exist as text near it. The container has no role and announces nothing on its own.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Overview
|
|
4
|
+
description: Three containers that reserve their aspect ratio before the content arrives, and stop at the container.
|
|
5
|
+
weight: 1
|
|
6
|
+
icon: book
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Three containers, all built on the same idea: hold the space before the content arrives.
|
|
10
|
+
|
|
11
|
+
| Page | Class | For |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| [Embed](../embed/) | `.embed` | Third-party iframes, players, social cards |
|
|
14
|
+
| [Video](../video/) | `.video` | A native `<video>`, with an optional pause control |
|
|
15
|
+
| [Map](../map/) | `.map` | A Leaflet map container |
|
|
16
|
+
|
|
17
|
+
## They all reserve an aspect ratio
|
|
18
|
+
|
|
19
|
+
An iframe, a video or a map that sizes itself once its content loads pushes the rest of the page down at an unpredictable moment. Each of these components declares `aspect-ratio` from a token, so the box exists at its final size from the first paint.
|
|
20
|
+
|
|
21
|
+
That is also why all three set `overflow: hidden`: the content is stretched to fill a box whose proportions it did not choose.
|
|
22
|
+
|
|
23
|
+
## They stop at the container
|
|
24
|
+
|
|
25
|
+
None of the three ships the thing it wraps. `.embed` gives you no player, `.video` gives you no controls beyond the [toggle button](../../buttons/btn-toggle-video/), and `.map` gives you neither Leaflet nor its stylesheet. Loading those, and keeping them out of your own layers, is the consuming project's job. See [Cascade layers](../../cascade-layers/) for where third-party CSS belongs.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Video
|
|
4
|
+
description: A wrapper for a native video, positioning the optional pause control an autoplaying video needs.
|
|
5
|
+
weight: 3
|
|
6
|
+
icon: camera-video
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
A wrapper for a **native** `<video>`, as opposed to an embedded player.
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<div class="video">
|
|
13
|
+
<video autoplay muted loop playsinline></video>
|
|
14
|
+
<button class="btn btn-toggle-video is-playing" aria-label="Pause">
|
|
15
|
+
<span class="icon-pause"></span>
|
|
16
|
+
<span class="icon-play"></span>
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What it does
|
|
22
|
+
|
|
23
|
+
Three things:
|
|
24
|
+
|
|
25
|
+
```css
|
|
26
|
+
.video {
|
|
27
|
+
position: relative;
|
|
28
|
+
|
|
29
|
+
video { aspect-ratio: var(--video-ratio, var(--ratio-video)); width: 100%; }
|
|
30
|
+
|
|
31
|
+
.btn-toggle-video { position: absolute; inset-block-end: 0; inset-inline-end: 0; z-index: 10; }
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
It establishes the positioning context, sizes the video to its container at a known ratio, and pins the control to the bottom inline-end corner. The ratio falls back to `--ratio-video` from the design tokens, so `--video-ratio` only needs setting for a portrait or square clip.
|
|
36
|
+
|
|
37
|
+
## The control is not decoration
|
|
38
|
+
|
|
39
|
+
A video that autoplays without native controls must be pausable to meet WCAG, which is exactly what [`.btn-toggle-video`](../../buttons/btn-toggle-video/) provides. If your video carries `controls`, you do not need it.
|
|
40
|
+
|
|
41
|
+
The button is the only child the wrapper positions. Anything else you put inside sits in the normal flow.
|
|
42
|
+
|
|
43
|
+
## Inside a media block
|
|
44
|
+
|
|
45
|
+
```css
|
|
46
|
+
.media .video { height: 100%; }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
A [`.media`](../../content/media/) stretches the wrapper to fill it, so a video and an image are interchangeable within a [card](../../content/card/) without changing the card's structure. The media block's own `--media-ratio` then governs the box, and the video is cropped to it by `object-fit: cover`.
|
|
50
|
+
|
|
51
|
+
## Autoplay checklist
|
|
52
|
+
|
|
53
|
+
`muted` is required for autoplay to be allowed at all, and `playsinline` keeps iOS from taking the video fullscreen. `loop` is what makes a background video a background video. All three are attributes on the element, not something this component can supply.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Check row
|
|
4
|
+
description: A checkbox or radio laid out beside its label, and why the class is only one declaration.
|
|
5
|
+
weight: 3
|
|
6
|
+
icon: check-square
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
A checkbox or radio laid out inline with its label.
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<div class="form-check">
|
|
13
|
+
<input id="terms" type="checkbox">
|
|
14
|
+
<label for="terms">I accept the terms</label>
|
|
15
|
+
</div>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## The whole component
|
|
19
|
+
|
|
20
|
+
```css
|
|
21
|
+
.form-check { display: flex; }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
That is the entire rule, and it is deliberate. The control's appearance, its size, and the gap between it and the adjacent label all come from [css-base](../../../css-base/base/form-checkbox/), which styles `[type='checkbox'] + label` directly on the native elements.
|
|
25
|
+
|
|
26
|
+
So the only thing missing from a bare input and label is that they sit on separate lines by default. This class fixes that, and stays out of everything else.
|
|
27
|
+
|
|
28
|
+
The practical consequence: if the spacing between a checkbox and its label is wrong, the fix is in css-base or in the tokens, not here. Adding a `gap` to `.form-check` would apply on top of the one css-base already sets, and the two would drift apart.
|
|
29
|
+
|
|
30
|
+
## The label must come after the control
|
|
31
|
+
|
|
32
|
+
`[type='checkbox'] + label` is an adjacent sibling selector. Wrapping the input inside the label, or putting the label first, means the control gets no spacing and no styling from css-base.
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<!-- works -->
|
|
36
|
+
<div class="form-check">
|
|
37
|
+
<input id="terms" type="checkbox">
|
|
38
|
+
<label for="terms">I accept the terms</label>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<!-- does not -->
|
|
42
|
+
<div class="form-check">
|
|
43
|
+
<label for="terms">I accept the terms</label>
|
|
44
|
+
<input id="terms" type="checkbox">
|
|
45
|
+
</div>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Inside a form grid
|
|
49
|
+
|
|
50
|
+
A check row is a direct child of [`.form`](../form/) like any other field, so it takes a grid track. Give it `.full` when the label is long enough to wrap in half the width:
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<div class="form-check full">
|
|
54
|
+
<input id="newsletter" type="checkbox">
|
|
55
|
+
<label for="newsletter">Send me the monthly newsletter</label>
|
|
56
|
+
</div>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Groups of radios
|
|
60
|
+
|
|
61
|
+
There is no class for the group. Use a `<fieldset>` with a `<legend>`, and one `.form-check` per option:
|
|
62
|
+
|
|
63
|
+
```html
|
|
64
|
+
<fieldset class="full">
|
|
65
|
+
<legend>Preferred contact</legend>
|
|
66
|
+
<div class="form-check">
|
|
67
|
+
<input id="by-email" type="radio" name="contact" value="email">
|
|
68
|
+
<label for="by-email">Email</label>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="form-check">
|
|
71
|
+
<input id="by-phone" type="radio" name="contact" value="phone">
|
|
72
|
+
<label for="by-phone">Phone</label>
|
|
73
|
+
</div>
|
|
74
|
+
</fieldset>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The legend is what names the group in the accessibility tree. A set of radios with individual labels and no legend is announced one option at a time, with nothing saying what is being chosen.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Form
|
|
4
|
+
description: A responsive grid that is one column or two, never three, plus its layout helpers.
|
|
5
|
+
weight: 2
|
|
6
|
+
icon: columns-gap
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
A responsive two-column grid that collapses to one column when there is not enough room.
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<form class="form">
|
|
13
|
+
<div>
|
|
14
|
+
<label for="first">First name</label>
|
|
15
|
+
<input id="first" type="text">
|
|
16
|
+
</div>
|
|
17
|
+
<div>
|
|
18
|
+
<label for="last">Last name</label>
|
|
19
|
+
<input id="last" type="text">
|
|
20
|
+
</div>
|
|
21
|
+
<div class="full">
|
|
22
|
+
<label for="message">Message</label>
|
|
23
|
+
<textarea id="message"></textarea>
|
|
24
|
+
<p class="help" id="message-help">Markdown is supported.</p>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="actions">
|
|
27
|
+
<button class="btn" type="submit">Send</button>
|
|
28
|
+
</div>
|
|
29
|
+
</form>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Each field is a plain `div`. There is no `.form-group` or `.field` class: the grid places its direct children, whatever they are.
|
|
33
|
+
|
|
34
|
+
## One column or two, never three
|
|
35
|
+
|
|
36
|
+
```css
|
|
37
|
+
grid-template-columns: repeat(
|
|
38
|
+
auto-fill,
|
|
39
|
+
minmax(max(var(--form-col-min-width, 25ch), calc(50% - var(--gap) / 2)), 1fr)
|
|
40
|
+
);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The `max()` is doing two jobs at once. It sets a **floor** of `25ch`, below which a field is too narrow to use, and it sets each track to at least half the width minus half the gap, which **caps** the result at two columns however wide the container gets.
|
|
44
|
+
|
|
45
|
+
A three-column form is almost never what you want, because it breaks the vertical reading order of a sequence of fields: the eye has to move right twice before dropping a line, and nothing in the layout says which order the fields belong in.
|
|
46
|
+
|
|
47
|
+
Change the floor with `--form-col-min-width`. Raising it past 50% forces a single column, which is a reasonable way to write a narrow form without a variant class.
|
|
48
|
+
|
|
49
|
+
## Layout helpers
|
|
50
|
+
|
|
51
|
+
| Class | Effect |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `.full` | Spans both columns, `grid-column: 1 / -1` |
|
|
54
|
+
| `.actions` | Spans both columns and stacks its children vertically |
|
|
55
|
+
| `.help` | Help text under a field |
|
|
56
|
+
|
|
57
|
+
`.actions` is a column flex container aligned to the start, with `--gap` between its children and their block margins flattened. So a submit button, a consent line and a legal note stack in the order written without fighting each other's margins.
|
|
58
|
+
|
|
59
|
+
`.help` reads `--help-*` for colour, size, weight, margin and alignment. Pair it with `aria-describedby`, or it is seen and never announced.
|
|
60
|
+
|
|
61
|
+
## Field spacing
|
|
62
|
+
|
|
63
|
+
Both axes use `--gap`, the same token the grid uses for its column calculation. That is why the two-column cap subtracts `var(--gap) / 2`: the track width and the gutter come from one value, so changing it cannot desynchronise them.
|
|
64
|
+
|
|
65
|
+
## Controls are not styled here
|
|
66
|
+
|
|
67
|
+
Inputs, selects, textareas and their focus and invalid states come from [css-base](../../../css-base/base/form/), applied to the native elements. This file adds layout and nothing else, which is why a form built without `.form` still looks right, it is simply stacked.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Overview
|
|
4
|
+
description: The two form layout components, why the controls themselves are styled elsewhere, and the accessible names none of it supplies.
|
|
5
|
+
weight: 1
|
|
6
|
+
icon: book
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Form **controls** are styled by [@uncinq/css-base](../../../css-base/base/form/), 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
|
+
| Page | Class | Covers |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| [Form](../form/) | `.form` | The two-column grid and its layout helpers |
|
|
16
|
+
| [Check row](../form-check/) | `.form-check` | A checkbox or radio laid out beside its label |
|
|
17
|
+
|
|
18
|
+
Both live in `components/form.css`.
|
|
19
|
+
|
|
20
|
+
## Accessibility
|
|
21
|
+
|
|
22
|
+
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.
|
|
23
|
+
|
|
24
|
+
Tie `.help` text to its field with `aria-describedby`, otherwise it is visible but never announced:
|
|
25
|
+
|
|
26
|
+
```html
|
|
27
|
+
<input id="message" aria-describedby="message-help">
|
|
28
|
+
<p class="help" id="message-help">Markdown is supported.</p>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For error states, use `aria-invalid="true"` on the control and point `aria-describedby` at the error message.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Breadcrumb
|
|
4
|
+
description: The breadcrumb trail, with the class on the wrapper and the separator drawn from a token.
|
|
5
|
+
weight: 5
|
|
6
|
+
icon: chevron-double-right
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
The breadcrumb trail. **The class goes on the wrapper, not on the list.**
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<nav class="breadcrumb-wrapper" aria-label="Breadcrumb">
|
|
13
|
+
<ol>
|
|
14
|
+
<li><a href="/">Home</a></li>
|
|
15
|
+
<li><a href="/docs">Docs</a></li>
|
|
16
|
+
<li aria-current="page">Navigation</li>
|
|
17
|
+
</ol>
|
|
18
|
+
</nav>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`.breadcrumb-wrapper` styles the `ol` and the `li` inside it by element, so there is no class to put on either. The name says wrapper for that reason.
|
|
22
|
+
|
|
23
|
+
## The separator
|
|
24
|
+
|
|
25
|
+
Drawn on `li::before` from a token, with the first item's suppressed:
|
|
26
|
+
|
|
27
|
+
```css
|
|
28
|
+
li::before { content: var(--breadcrumb-separator); color: var(--breadcrumb-color-separator); }
|
|
29
|
+
li:first-child::before { display: none; }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`--breadcrumb-separator` defaults to `"/"` and takes any `content` value, so a chevron or an arrow is a token change. It needs the quotes, since it is dropped into `content` as is.
|
|
33
|
+
|
|
34
|
+
Because the separator is generated content rather than text, it is not selected when a reader copies the trail, and screen readers skip it.
|
|
35
|
+
|
|
36
|
+
## Marking the current page
|
|
37
|
+
|
|
38
|
+
Use `aria-current="page"`, which is both the accessible marker and the styling hook:
|
|
39
|
+
|
|
40
|
+
```css
|
|
41
|
+
li[aria-current="page"] { color: var(--breadcrumb-color-text-active); }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
There is no `.active` class here, deliberately. Relying on the attribute means the visual state and the announced state cannot disagree, and it rules out the other common approach, styling `:last-child`, which breaks the moment a trail ends on something other than the current page.
|
|
45
|
+
|
|
46
|
+
## Overflow on narrow screens
|
|
47
|
+
|
|
48
|
+
```css
|
|
49
|
+
overflow-x: auto;
|
|
50
|
+
li { flex-shrink: 0; white-space: nowrap; }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
A long trail scrolls sideways rather than wrapping into several lines or truncating. Items keep their full width, so the deepest entries stay readable and the reader scrolls to reach the shallow ones.
|
|
54
|
+
|
|
55
|
+
The wrapper also carries a block border and its own background, which is what lets a breadcrumb sit as a full-width band under the header.
|
|
56
|
+
|
|
57
|
+
## Accessibility
|
|
58
|
+
|
|
59
|
+
`aria-label` on the `<nav>` is what distinguishes this landmark from the other navigation regions on the page. Use an `<ol>`, not a `<ul>`: the order of a breadcrumb is its meaning.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Skip links
|
|
4
|
+
description: Skip navigation for keyboard and assistive technology users, hidden off-screen rather than hidden.
|
|
5
|
+
weight: 4
|
|
6
|
+
icon: universal-access
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Skip links, for keyboard and assistive technology users.
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<ul class="nav nav-accessibility">
|
|
13
|
+
<li><a href="#main">Skip to content</a></li>
|
|
14
|
+
<li><a href="#navigation">Skip to navigation</a></li>
|
|
15
|
+
<li><a href="#footer">Skip to footer</a></li>
|
|
16
|
+
</ul>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Three things matter here, and getting any of them wrong defeats the purpose.
|
|
20
|
+
|
|
21
|
+
## It must be the first focusable element in `<body>`
|
|
22
|
+
|
|
23
|
+
A skip link that comes after the navigation it skips is useless. It goes before the header, before any cookie banner, before anything else that can take focus.
|
|
24
|
+
|
|
25
|
+
## It is hidden off-screen, not hidden
|
|
26
|
+
|
|
27
|
+
```css
|
|
28
|
+
.nav-accessibility {
|
|
29
|
+
position: absolute;
|
|
30
|
+
transform: translateY(-100%);
|
|
31
|
+
|
|
32
|
+
&:focus-within { transform: translateY(0); }
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`display: none` or `visibility: hidden` would remove it from the tab order entirely, which is the one thing a skip link cannot afford. It is pushed out of view with a transform and slides back on `:focus-within`, so the whole group appears as soon as any link inside it is reached.
|
|
37
|
+
|
|
38
|
+
The slide is behind `prefers-reduced-motion`; with motion disabled the group simply appears.
|
|
39
|
+
|
|
40
|
+
## It requires the `.nav` base class
|
|
41
|
+
|
|
42
|
+
`.nav-accessibility` sets `flex-direction: row` and a small font size, but the flex layout, the list reset and the link padding all come from [`.nav`](../nav/). On its own it is an unstyled list positioned off-screen.
|
|
43
|
+
|
|
44
|
+
## The focus treatment is deliberate
|
|
45
|
+
|
|
46
|
+
```css
|
|
47
|
+
a {
|
|
48
|
+
background: var(--color-background);
|
|
49
|
+
&:focus {
|
|
50
|
+
background: var(--color-text);
|
|
51
|
+
color: var(--color-background);
|
|
52
|
+
outline: none;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The resting background is what keeps the links readable once the group slides over the page content. On focus the colours invert, which is why the usual focus ring is dropped: the inversion is the focus indicator, and it meets the contrast requirement on its own.
|
|
58
|
+
|
|
59
|
+
Note this uses `:focus`, not `:focus-visible`, so the indicator appears however focus arrived.
|
|
60
|
+
|
|
61
|
+
## The targets have to be focusable
|
|
62
|
+
|
|
63
|
+
`z-index: 300` puts the group above sticky headers, but a working skip link also needs somewhere to land. An `id` on a non-interactive element needs `tabindex="-1"`, otherwise the jump scrolls the page without moving focus, and the next Tab press continues from the top of the document:
|
|
64
|
+
|
|
65
|
+
```html
|
|
66
|
+
<main id="main" tabindex="-1">
|
|
67
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Nav title
|
|
4
|
+
description: A label above a group of navigation links.
|
|
5
|
+
weight: 3
|
|
6
|
+
icon: type
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
A title within a navigation block, for labelling a group of links.
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<nav aria-labelledby="docs-nav">
|
|
13
|
+
<p class="nav-title" id="docs-nav">Documentation</p>
|
|
14
|
+
<ul class="nav">
|
|
15
|
+
<li><a href="/docs/install">Installation</a></li>
|
|
16
|
+
<li><a href="/docs/usage">Usage</a></li>
|
|
17
|
+
</ul>
|
|
18
|
+
</nav>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
It is a leaf component with no children and no variants:
|
|
22
|
+
|
|
23
|
+
```css
|
|
24
|
+
.nav-title {
|
|
25
|
+
font-size: var(--nav-title-font-size);
|
|
26
|
+
font-weight: var(--nav-title-font-weight);
|
|
27
|
+
margin-block: var(--nav-title-margin-block, 0);
|
|
28
|
+
padding-block: var(--nav-title-padding-block);
|
|
29
|
+
padding-inline: var(--nav-title-padding-inline);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## It is a sibling of the list, not an item in it
|
|
34
|
+
|
|
35
|
+
Putting it inside the `<ul>` as an `<li>` would make it a list item that screen readers count and announce as one, and [`.nav`](../nav/) would style it as a navigation entry. Keep it outside.
|
|
36
|
+
|
|
37
|
+
## Why it carries inline padding
|
|
38
|
+
|
|
39
|
+
`--nav-title-padding-inline` exists so the label lines up with the link text below it, which is itself inset by `--nav-link-padding-inline`. Keep the two in step when you change either, or the title sits proud of the list it labels.
|
|
40
|
+
|
|
41
|
+
## Use it as the accessible name
|
|
42
|
+
|
|
43
|
+
A group of links deserves a label in the accessibility tree as well as on screen. Point the surrounding `<nav>` at it with `aria-labelledby` rather than repeating the text in an `aria-label`, so the two cannot drift apart.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Nav
|
|
4
|
+
description: The base navigation list, vertical by default, whose direction is set by context rather than by a modifier class.
|
|
5
|
+
weight: 2
|
|
6
|
+
icon: compass
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
The base navigation list, vertical by default.
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<ul class="nav">
|
|
13
|
+
<li><a href="/">Home</a></li>
|
|
14
|
+
<li><a href="/about">About</a></li>
|
|
15
|
+
</ul>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Works on `ul` or `ol`.
|
|
19
|
+
|
|
20
|
+
## Direction is set by context, not by a modifier
|
|
21
|
+
|
|
22
|
+
There is no `.nav-horizontal`. The direction comes from a token:
|
|
23
|
+
|
|
24
|
+
```css
|
|
25
|
+
flex-direction: var(--nav-direction, column);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
so a header lays its own navigation out horizontally by setting `--nav-direction: row` on it, or `flex-direction` directly. That keeps the component free of layout variants that would only ever be used in one place, and it means the same list is a column in a drawer and a row in a header without a class change.
|
|
29
|
+
|
|
30
|
+
## Nested lists inherit the layout
|
|
31
|
+
|
|
32
|
+
```css
|
|
33
|
+
&,
|
|
34
|
+
:is(ul, ol) { display: flex; flex-direction: var(--nav-direction, column); ... }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The rule matches the nav **and** every list inside it, so a submenu needs no second `.nav` class. Note that a nested list inherits `--nav-direction` too, so a horizontal header menu gets horizontal submenus unless the submenu resets it.
|
|
38
|
+
|
|
39
|
+
## Links
|
|
40
|
+
|
|
41
|
+
`a:where(:not(.btn))` and `li > span` are styled together, so a non-link entry, the current page rendered as text, lines up with the links around it. `.btn` is excluded through `:where()`, which adds no specificity, so a call to action inside a navigation keeps its own styling.
|
|
42
|
+
|
|
43
|
+
The current entry is marked with a class on the link:
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<li><a href="/about" class="active" aria-current="page">About</a></li>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```css
|
|
50
|
+
&.active { --nav-color-text: var(--nav-color-text-active); }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The class drives the colour; `aria-current` is what a screen reader announces. Write both.
|
|
54
|
+
|
|
55
|
+
## Highlighted items
|
|
56
|
+
|
|
57
|
+
`.nav-item-highlighted` tightens the line height and pulls the item out to the nav's own inline padding, for an entry rendered as a block rather than as a line of text.
|
|
58
|
+
|
|
59
|
+
## Dropdowns inside a nav
|
|
60
|
+
|
|
61
|
+
`.dropdown-toggle` is restyled to take `--nav-color-text`, so a [dropdown](../../overlays/dropdown/) in a header menu matches the links beside it. Nothing else about the dropdown changes.
|
|
62
|
+
|
|
63
|
+
## Inside a panel
|
|
64
|
+
|
|
65
|
+
A `.modal` or `.drawer` publishes `--nav-color-text` from its own text colour, so a navigation inside a panel follows the panel's palette. Going inline with [`.panel-inline-*`](../../overlays/panel/#inline-variants) hands that token back to the page, which is why a drawer menu that becomes a sidebar column changes colour at the breakpoint.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
isIndex: false
|
|
3
|
+
title: Overview
|
|
4
|
+
description: The five navigation components, the two that take their class on the wrapper, and the landmarks none of them supplies.
|
|
5
|
+
weight: 1
|
|
6
|
+
icon: book
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Five components for moving around a site.
|
|
10
|
+
|
|
11
|
+
| Page | Class | Purpose |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| [Nav](../nav/) | `.nav` | The base navigation list |
|
|
14
|
+
| [Nav title](../nav-title/) | `.nav-title` | A label above a group of links |
|
|
15
|
+
| [Skip links](../nav-accessibility/) | `.nav-accessibility` | Skip navigation for keyboard and assistive technology |
|
|
16
|
+
| [Breadcrumb](../breadcrumb/) | `.breadcrumb-wrapper` | The breadcrumb trail |
|
|
17
|
+
| [Pagination](../pagination/) | `.pagination` | Page navigation |
|
|
18
|
+
|
|
19
|
+
## Two of them take the class on the wrapper
|
|
20
|
+
|
|
21
|
+
`.breadcrumb-wrapper` goes on the `<nav>`, not on the `<ol>` inside it. `.nav-accessibility` goes on the list itself, but only alongside `.nav`. Neither is guessable from the name, so check the page before writing the markup.
|
|
22
|
+
|
|
23
|
+
## None of them supplies the landmark
|
|
24
|
+
|
|
25
|
+
`<nav>`, `aria-label` and `aria-current` are yours to write. A page with three navigation regions and no labels on them is harder to use with a screen reader than a page with one, which is the usual reason to add them.
|
|
26
|
+
|
|
27
|
+
## `.item` means two different things
|
|
28
|
+
|
|
29
|
+
In [pagination](../pagination/), `.item` is a page number. It is unrelated to the `.item` card unit mentioned throughout the [Content](../../content/) section, which is not defined in this package at all.
|