Skip to content

src/nav - 9 files

Navigation

Navigation is mostly behaviour, and behaviour is the consumer's. Every component here styles an ARIA attribute rather than a class, so the thing the eye sees and the thing a screen reader announces cannot drift apart.

Every example below is operable. Open the menus, switch the tabs with the arrow keys, push the drawer open and close it with Escape, click a page number and watch aria-current move. The bar and the strip sit in frames with a viewport of their own and a slider under them, because their shape is keyed to a media query and no amount of narrowing a div inside this document changes what a media query in this document evaluates to.

The markup panels below are read out of the live examples by site.js, and the frames and width controls come from src/behaviour/demo-stage.js. With scripting off every example still renders, the framed ones render inline at their natural width, and the panels stay empty.

Rail

A vertical navigation rail for documentation and application sections. Full width at 320px, a column beside the content from 64em. The rail to the left of this text is the same component.

A nested list goes inside the list item it belongs to, not beside it: that is what makes the hierarchy real rather than a visual indent, and what a screen reader announces as a nested list. If sections collapse, the group heading is a button with aria-expanded, never a paragraph.

Railstrip

The rail's answer for a phone. A vertical rail on a small screen has only bad options: a tall list that pushes the content off the first screen, or a hamburger that hides the structure so nobody finds the other sections. The strip keeps every section permanently visible for the cost of one row, and it is the control a phone user already knows: flick it sideways. The strip under the bar at the top of this page is this component. Drag it, or narrow the box below until it has to scroll.

The list never wraps, because a strip whose items wrap has silently become the tall list it exists to replace. It snaps on proximity rather than mandatorily, so a small deliberate drag is not fought. overscroll-behavior-inline: contain is what stops a flick that reaches the end from handing its momentum to the document and rubber-banding the whole page sideways on iOS. --bs-railstrip-bleed defaults to zero on purpose: only the consumer knows their container's padding, and guessing it put eight pixels of horizontal scroll on every page here. Add --mobile-only when the page also renders a vertical rail past 64em, and leave it off when the strip is the only navigation.

Tabs

Almost entirely keyboard behaviour, none of which can come from CSS. Try it: Tab from outside lands on the selected tab and Tab again leaves the strip for the panel. Left and Right move between tabs, Home and End jump to the ends.

src/nav/tabs.css, adds 2,720 B gzip

Your name, your handle and the address receipts go to.

Show codeHide code

Roving tabindex is the part implementations get wrong: exactly one tab is in the tab sequence, the selected one at tabindex="0" and every other at -1. Inactive panels are hidden with the hidden attribute, not a class, because the reset enforces [hidden] above the component layer. The indicator is the only animated thing here and it moves on transform alone.

Drawer

An off-canvas panel that slides in from one edge, built on native dialog. Open it with showModal(), never with show() and never by setting the open attribute by hand.

src/nav/drawer.css, adds 2,854 B gzip
Show codeHide code

showModal() is what gets you, from the browser rather than from script: focus moved into the dialog and trapped there, everything behind it made inert, Escape closing it, and the backdrop this file paints. show() and the bare attribute give you none of that and leave a dialog a keyboard user can Tab straight out of. Opening slides on transform alone; the closing slide is the consumer's, because the element leaves the top layer the instant close() is called.

Pagination

Page-by-page navigation for a paged list. It wraps at 320px rather than scrolling, so no page number is ever off screen. Click a digit: the current marker moves, and so does the accessible name, because both are attributes rather than paint.

src/nav/pagination.css, adds 1,862 B gzip
auto
Show codeHide code

Bare digits are poor link text, so aria-label gives each link a real accessible name while the visible text stays a digit. The gap is decorative, its list item carries aria-hidden="true", and it is three dots rather than an ellipsis character. Never ship a disabled anchor: it stays clickable. Omit the link or render a span with aria-disabled="true".