Navigation
Wayfinding, at any width
Every component on this page responds to its container, not to the browser window. Use the width buttons on each specimen to check the narrow state — which is where responsive bugs live and where nobody looks unless it takes two seconds.
Information: Why the width switcher works at all
The stage below each toolbar is narrowed with
inline-size. A component whose layout
comes from @container measures the stage
and responds. One using @media measures
the window, which has not changed — so the buttons would visibly do
nothing. That is the practical reason DDS requires container queries
for anything with layout-shifting behaviour.
Site header
One navigation, one place in the DOM. The narrow and wide layouts are the same markup — a separate “mobile menu” is a second copy that drifts, and a screen reader announces both.
Opt-in drawer
Add data-dds-drawer to the toggle and a
sibling .dds-siteheader-scrim and the
collapsed nav opens as a modal panel instead of pushing the page down:
scrim, scroll-lock, inert content, a
data-dds-nav-close button first in the
panel, Escape. Narrow the width switcher below 48rem, open it, then try
Tab and Escape.
Below 48rem of container width the links collapse behind the menu
button; above it they sit inline and the button disappears. In the
collapsed row the menu button is pinned last, hard against the
inline-end edge; the search and account actions sit immediately to its
left as one group. Opening the nav cross-fades the button from the menu
icon to a close icon. By default the open nav takes its own row in
flow; data-dds-drawer on the toggle makes
it a modal panel instead (above). The current page is marked with
aria-current="page" and carries colour,
weight and a rule — three cues.
Do
- Use one
<nav>with a name, in one DOM location. - Pair the button with
aria-expandedandaria-controls. - Close on Escape and return focus to the button.
- Mark the current page programmatically, not only visually.
Don’t
- Render a second copy of the navigation for small screens.
- Use
role="menu"for a list of page links. - Hide the collapsed nav with CSS only — it leaves invisible tab stops.
- Rely on
@media, or the header breaks inside a panel.
Breadcrumb
The separator is a CSS pseudo-element, so it is not in the accessibility tree — a literal “/” would be read out between every item. The last entry is not a link: linking a page to itself is a dead control.
Scrollable middle (#146): once there is more than
one level between the root and the current page, they sit in their
own nested <ol> inside
.dds-breadcrumb-scroll-track —
overflow-x: auto, inert wherever the
middle already fits, the same way .dds-pagination’s
own numbers strip scrolls below a width. First and last never
shrink or hide. No tab stop of its own: focusing a link scrolls it
into view natively, so a keyboard user reaches every level in the
same tab order. A partially visible label at the trailing edge is
its own affordance, the same cut-off cue the pagination strip
below relies on.
Pagination
Navigation between pages of a list, as real links with real hrefs, so a page can be bookmarked, shared and opened in a new tab.
Each name says where it goes — “Page 3”, not “3”.
Below 28rem of container width: the numbers stop wrapping and scroll instead, so Previous and Next stay on the line with them. Wrapping put “Previous” alone on the first line and read as three controls rather than one. No page is dropped and nothing hides behind a “more” control — the strip needs no tab stop of its own because every child in it is a link, and focusing a link scrolls it into view.
Step progress
Each step states its status in words. A row of coloured dots communicates nothing without sight, and “which step am I on” is essential orientation rather than decoration.
- Your details Completed
- Address Completed
- Documents Current step
- Review and submit Not started
Stacked below 34rem of container width, horizontal above it. Completed steps may be links if the user is allowed back; upcoming ones never are.
Toolbar
The menu is a popover invoked by
popovertarget — no JavaScript. Light
dismiss, Escape, top-layer rendering and focus return all come from
the platform.
Narrow: the row wraps, and the rule between groups sits on the trailing edge of the group before the gap. On the leading edge of the one after it, a wrapped group began the new line with a vertical rule separating it from nothing. Groups keep their spacing either way, so the grouping survives the break even where the rule does not fall between two groups on one line.
Filter bar
Controls that narrow a list. A real form, so the state can live in the URL and the result is shareable.
Applied filters must always be visible as removable chips. A short list with three invisible filters applied is the most common dead end in a filtered interface — the user concludes the data is missing.
Action bar
A sticky bar carrying the actions for what is on screen — a form's Save, a selection's operations.
In use it is position: sticky with
env(safe-area-inset-bottom), and the
scrolling ancestor carries
.dds-actionbar-host so the bar never
covers a field that has just been focused (WCAG 2.2 2.4.11).
Table of contents
The active entry is aria-current="location",
not "page". The user has not navigated
anywhere — the reading position moved.
App topbar
Application chrome, as opposed to site chrome. A site header is for getting between pages; a topbar names what you are looking at, holds the actions that apply to it, and stays put while the content scrolls.
The title is a heading
.dds-topbar-title goes on a real heading
element, so the object's name is in the document outline. A styled
div looks identical and leaves a
screen-reader user with no way to find out what they are editing.
It truncates with an ellipsis rather than wrapping, so a long name cannot push the actions off the screen. Truncation is only safe because the full name is also the page heading below — a topbar is the summary, never the only place the name appears.
Below 34rem of container width: the actions take a row
of their own and wrap among themselves; the menu button and the title
keep the first row. The title only truncates because
.dds-topbar-context has
flex-basis: 0 — a flex container breaks
lines on each item's hypothetical size, so with
auto the context wrapped to its own line
at its full max-content width and shrinking was never considered. An
item has to be willing to be narrow, not merely able to be.
Alert banner
One page-level or application-level message: planned downtime, a degraded service, an expiring credential. Only ever one at a time — two stacked means neither is read.
The frame is position: sticky in the
component; it is overridden to static here
so it stays inside this example instead of following you down the page.
Alert banner
Page-level, edge to edge, and only ever one at a time. Two stacked banners means neither gets read. Distinct from a notice, which attaches to the one section it concerns.
Which role, and when none
All four are shown stacked here so they can be compared. That is the one situation in which stacking them is right.
- Already there when the page loaded — no role at all. A live region present at load gets announced before the page itself, so the first thing a screen-reader user hears is the banner, out of any context.
-
Appeared just now, informational —
role="status". Announced politely, after whatever is being read finishes. -
Appeared just now and blocks what the user was doing —
role="alert". This interrupts, so it has to be worth interrupting for.
The dismiss button needs a label that says what it dismisses. "Dismiss" alone, in a list of links and buttons read out of context, gives no clue which of several things it closes.