Navs and tabs
A row of links between views of one thing - nav-underline - or of choices - nav-pills; and the same row as tabs that switch panels on the page, driven by the keyboard as the WAI-ARIA tabs pattern.
Examples
Links between pages are a <nav>; the current one is aria-current="page".
<nav class="nav nav-underline" aria-label="List views">
<a class="nav-link" href="#nav-links" aria-current="page">All<span class="badge">28</span></a>
<a class="nav-link" href="#nav-links">Needs attention<span class="badge badge-subtle-warning">3</span></a>
<a class="nav-link" href="#nav-links">Drafts</a>
</nav>
<nav class="nav nav-pills" aria-label="Period">
<a class="nav-link" href="#nav-links" aria-current="page">Week</a>
<a class="nav-link" href="#nav-links">Month</a>
<a class="nav-link" href="#nav-links">Year</a>
</nav>Tabs
Tabs switch panels in place: role="tablist", each tab role="tab" with aria-selected and aria-controls, each panel role="tabpanel". The library keeps only the selected tab in the tab order and only its panel shown. The arrow keys select as they move.
<div class="nav nav-underline" role="tablist" aria-label="Excursion">
<button class="nav-link" type="button" role="tab" id="nav-tabs-1" aria-selected="true" aria-controls="nav-tabs-panel-1">Details</button>
<button class="nav-link" type="button" role="tab" id="nav-tabs-2" aria-selected="false" aria-controls="nav-tabs-panel-2">Prices</button>
<button class="nav-link" type="button" role="tab" id="nav-tabs-3" aria-selected="false" aria-controls="nav-tabs-panel-3">History</button>
</div>
<div id="nav-tabs-panel-1" role="tabpanel" aria-labelledby="nav-tabs-1" tabindex="0"><p>A day at sea: snorkelling at two reefs, lunch on the boat.</p></div>
<div id="nav-tabs-panel-2" role="tabpanel" aria-labelledby="nav-tabs-2" tabindex="0" hidden><p>Adults 2,500 · children 1,900 · under four free.</p></div>
<div id="nav-tabs-panel-3" role="tabpanel" aria-labelledby="nav-tabs-3" tabindex="0" hidden><p>Edited yesterday by M. K.; published on three sites.</p></div>A tab with changes
data-changed, set by the page, puts a dot on a tab whose panel holds something not saved yet, the same word as on a field. Its aria-label says so to a screen reader.
<div class="nav nav-underline" role="tablist" aria-label="Settings">
<button class="nav-link" type="button" role="tab" id="nav-changed-1" aria-selected="true" aria-controls="nav-changed-panel-1">Company</button>
<button class="nav-link" type="button" role="tab" id="nav-changed-2" aria-selected="false" aria-controls="nav-changed-panel-2" aria-label="Mail, not saved" data-changed>Mail</button>
</div>
<div id="nav-changed-panel-1" role="tabpanel" aria-labelledby="nav-changed-1" tabindex="0"><p>The company's name and address.</p></div>
<div id="nav-changed-panel-2" role="tabpanel" aria-labelledby="nav-changed-2" tabindex="0" hidden><p>The address mail is sent from.</p></div>Tabs whose panels load
When a panel comes from the server, the arrows should not load every panel they pass: data-activation="manual" on the tablist makes them move the focus only, and Enter or Space selects.
<div class="nav nav-pills" role="tablist" aria-label="Report" data-activation="manual">
<button class="nav-link" type="button" role="tab" id="nav-manual-1" aria-selected="true" aria-controls="nav-manual-panel-1">Bookings</button>
<button class="nav-link" type="button" role="tab" id="nav-manual-2" aria-selected="false" aria-controls="nav-manual-panel-2">Revenue</button>
<button class="nav-link" type="button" role="tab" id="nav-manual-3" aria-selected="false" aria-controls="nav-manual-panel-3">Refunds</button>
</div>
<div id="nav-manual-panel-1" role="tabpanel" aria-labelledby="nav-manual-1" tabindex="0"><p>128 bookings this week.</p></div>
<div id="nav-manual-panel-2" role="tabpanel" aria-labelledby="nav-manual-2" tabindex="0" hidden><p>312,400 THB this week.</p></div>
<div id="nav-manual-panel-3" role="tabpanel" aria-labelledby="nav-manual-3" tabindex="0" hidden><p>2 refunds this week.</p></div>A tab that asks before it is left
Before a person's click or key selects a tab, the tablist fires the cancelable redz:select; its detail holds tab, the one asked for, and from, the one being left. A page that must ask first - a form with unsaved edits - calls event.preventDefault() and event.detail.respondWith(promise): the tab is selected when the promise gives true, and otherwise the one being left stays, with the focus. Change the note below, then choose another tab.
<div class="nav nav-underline" role="tablist" aria-label="Excursion">
<button class="nav-link" type="button" role="tab" id="nav-guard-1" aria-selected="true" aria-controls="nav-guard-panel-1">Note</button>
<button class="nav-link" type="button" role="tab" id="nav-guard-2" aria-selected="false" aria-controls="nav-guard-panel-2">Prices</button>
</div>
<div id="nav-guard-panel-1" role="tabpanel" aria-labelledby="nav-guard-1" tabindex="0">
<label class="form-label" for="nav-guard-note">A note for the guides</label>
<input class="form-control" id="nav-guard-note" type="text" value="Meet at the pier at 8:00">
</div>
<div id="nav-guard-panel-2" role="tabpanel" aria-labelledby="nav-guard-2" tabindex="0" hidden><p>2,900 THB an adult.</p></div>tablist.addEventListener('redz:select', (event) => {
if (form.dataset.changed !== undefined) {
event.preventDefault();
event.detail.respondWith(REDZ.ui.confirm({title: 'Leave without saving?', ok: 'Leave'}));
}
});
Tabs in the address
A tablist with data-hash follows the address: location.hash naming a panel, or anything inside one, selects its tab - when the page loads and when the hash changes, a tablist inside a panel too -, and a person's choice writes the panel's id into the address, so a link or a reload opens the same tab. After every selection, the first one too, the tablist fires redz:selected with detail.tab. REDZ.ui.select(tab) chooses a tab from code as a person would - through redz:select, so the page can still refuse - and answers a promise of whether it was selected.
<div class="nav nav-underline" role="tablist" aria-label="Booking" data-hash>
<button class="nav-link" type="button" role="tab" id="nav-hash-1" aria-selected="true" aria-controls="nav-hash-guests">Guests</button>
<button class="nav-link" type="button" role="tab" id="nav-hash-2" aria-selected="false" aria-controls="nav-hash-payment">Payment</button>
<button class="nav-link" type="button" role="tab" id="nav-hash-3" aria-selected="false" aria-controls="nav-hash-history">History</button>
</div>
<div id="nav-hash-guests" role="tabpanel" aria-labelledby="nav-hash-1" tabindex="0"><p>Two adults, one child.</p></div>
<div id="nav-hash-payment" role="tabpanel" aria-labelledby="nav-hash-2" tabindex="0" hidden><p>Paid in full by card on 2 November.</p></div>
<div id="nav-hash-history" role="tabpanel" aria-labelledby="nav-hash-3" tabindex="0" hidden><p>Booked on 1 November; the date moved once.</p></div>
<p><a href="#nav-hash-payment">The payment</a> opens its tab.</p>tablist.addEventListener('redz:selected', (event) => load(event.detail.tab));
await REDZ.ui.select(document.getElementById('nav-hash-3'));
Filling the width
nav-fill lets the links share the width by their length, nav-justified in equal parts.
<nav class="nav nav-underline nav-fill" aria-label="Filled">
<a class="nav-link" href="#nav-fill" aria-current="page">Overview</a>
<a class="nav-link" href="#nav-fill">Prices and seasons</a>
<a class="nav-link" href="#nav-fill">Reviews</a>
</nav>
<nav class="nav nav-underline nav-justified" aria-label="Justified">
<a class="nav-link" href="#nav-fill" aria-current="page">Overview</a>
<a class="nav-link" href="#nav-fill">Prices and seasons</a>
<a class="nav-link" href="#nav-fill">Reviews</a>
</nav>Where you are on the page
A nav or a list group with data-scrollspy marks the link to the section in view aria-current="location" as the page - or the box the sections scroll in - scrolls: the last section whose top has passed a quarter of the height, or the last one at the end.
<div class="grid md:cols-12 gap-16">
<nav class="list-group list-group-flush md:span-4" aria-label="On this example" data-scrollspy>
<a class="list-group-item list-group-item-action" href="#nav-spy-plan">Plan</a>
<a class="list-group-item list-group-item-action" href="#nav-spy-boat">The boat</a>
<a class="list-group-item list-group-item-action" href="#nav-spy-lunch">Lunch</a>
<a class="list-group-item list-group-item-action" href="#nav-spy-back">Back</a>
</nav>
<div class="docs-scroll md:span-8 vstack gap-16" tabindex="0" role="region" aria-label="The day, by section">
<section id="nav-spy-plan" class="vstack gap-4"><h4 class="fs-14">Plan</h4><p class="mb-0">Pick-up at the hotel at 7:30, the pier at 8:15. The crew hands out life vests and snorkels before the boat leaves the bay.</p></section>
<section id="nav-spy-boat" class="vstack gap-4"><h4 class="fs-14">The boat</h4><p class="mb-0">A speedboat for 30 guests with shade and a toilet; the ride to the park takes an hour across open water.</p></section>
<section id="nav-spy-lunch" class="vstack gap-4"><h4 class="fs-14">Lunch</h4><p class="mb-0">Lunch on a beach of the park: rice, green curry, fruit and water; vegetarian on request.</p></section>
<section id="nav-spy-back" class="vstack gap-4"><h4 class="fs-14">Back</h4><p class="mb-0">The boat leaves the park at 15:30 and reaches the pier by 17:00; the transfer brings everyone back to the hotel.</p></section>
</div>
</div>A row that scrolls
A nav or a tab list wider than its box scrolls sideways, and the edge it goes on past fades - data-more says which, start, end or both, as the person scrolls; when the page loads, the current or selected item is scrolled into the row's view, the page itself left where it is.
<nav class="nav nav-underline max-w-320" aria-label="Task views">
<a class="nav-link" href="#nav-scrolling">All <span class="badge">48</span></a>
<a class="nav-link" href="#nav-scrolling">New <span class="badge">6</span></a>
<a class="nav-link" href="#nav-scrolling">Doing <span class="badge">9</span></a>
<a class="nav-link" href="#nav-scrolling">Checking <span class="badge">4</span></a>
<a class="nav-link" href="#nav-scrolling" aria-current="page">Done <span class="badge">25</span></a>
<a class="nav-link" href="#nav-scrolling">Returned <span class="badge">3</span></a>
<a class="nav-link" href="#nav-scrolling">Cancelled <span class="badge">1</span></a>
</nav>When the links do not fit
nav-overflow around a nav keeps it on one line: the links that do not fit hide from the end and come back, in their order, as the items of a menu behind a “More” button after the nav. The link the person is on and a link with nav-overflow-keep stay in the row. A link that opens a menu folds with it, as a group of its items under the link's words, and stays in the row while its menu holds the page the person is on; a navbar shows one. A link the page hides with hidden leaves the row and the menu, and comes back when the page shows it. It measures the room its own box has, not the window, so a nav in a narrow column folds as early as it needs to. For a row of links the wrapper is the <nav>, so the menu stays inside the landmark.
<nav class="nav-overflow max-w-480" aria-label="Excursion sections">
<div class="nav nav-underline">
<a class="nav-link" href="#nav-overflow" aria-current="page">Overview</a>
<a class="nav-link" href="#nav-overflow">Program</a>
<a class="nav-link" href="#nav-overflow">Prices</a>
<a class="nav-link" href="#nav-overflow">Dates</a>
<a class="nav-link" href="#nav-overflow">Pickup</a>
<a class="nav-link" href="#nav-overflow">Reviews<span class="badge">42</span></a>
<a class="nav-link" href="#nav-overflow">Photos</a>
<a class="nav-link" href="#nav-overflow">Questions</a>
</div>
</nav>Tabs fold the same way. The arrows move between the tabs in the row; a tab chosen in the menu comes back into the row selected, with the focus, and the last one in the row takes its place in the menu.
<div class="nav-overflow max-w-320">
<div class="nav nav-pills" role="tablist" aria-label="Guest">
<button class="nav-link nav-overflow-keep" type="button" role="tab" id="nav-overflow-tabs-1" aria-selected="true" aria-controls="nav-overflow-tabs-panel-1">Profile</button>
<button class="nav-link" type="button" role="tab" id="nav-overflow-tabs-2" aria-selected="false" aria-controls="nav-overflow-tabs-panel-2">Bookings</button>
<button class="nav-link" type="button" role="tab" id="nav-overflow-tabs-3" aria-selected="false" aria-controls="nav-overflow-tabs-panel-3">Payments</button>
<button class="nav-link" type="button" role="tab" id="nav-overflow-tabs-4" aria-selected="false" aria-controls="nav-overflow-tabs-panel-4">Messages</button>
<button class="nav-link" type="button" role="tab" id="nav-overflow-tabs-5" aria-selected="false" aria-controls="nav-overflow-tabs-panel-5">Notes</button>
</div>
</div>
<div id="nav-overflow-tabs-panel-1" role="tabpanel" aria-labelledby="nav-overflow-tabs-1" tabindex="0"><p>Anna Sample, two trips this year.</p></div>
<div id="nav-overflow-tabs-panel-2" role="tabpanel" aria-labelledby="nav-overflow-tabs-2" tabindex="0" hidden><p>North Bay on 12 December, 2 adults.</p></div>
<div id="nav-overflow-tabs-panel-3" role="tabpanel" aria-labelledby="nav-overflow-tabs-3" tabindex="0" hidden><p>Paid in full by card.</p></div>
<div id="nav-overflow-tabs-panel-4" role="tabpanel" aria-labelledby="nav-overflow-tabs-4" tabindex="0" hidden><p>No messages yet.</p></div>
<div id="nav-overflow-tabs-panel-5" role="tabpanel" aria-labelledby="nav-overflow-tabs-5" tabindex="0" hidden><p>Vegetarian lunch.</p></div>States
Keyboard and accessibility
| Key | In a tablist |
|---|---|
| → / ← | The next / the previous tab, round the ends - mirrored right to left; ↓ / ↑ in a list with aria-orientation="vertical" |
| Home / End | The first / the last tab |
| Enter, Space | Selects the focused tab - what the arrows do not, with data-activation="manual" |
| Tab | From the selected tab into its panel, or first to the “More” button when the row has one |
- Links that change the page are a
<nav>of links, never tabs: tabs only switch what is already on the page. - A link folded into the “More” menu is hidden in the row, so a screen reader and Tab meet it once, in the menu. The button says the page's text
more, reports whether its menu is open, and opens it by the menu's keys; a plain click on an item clicks the hidden link itself, so the page's own handlers run. - A mark beside a tab's words - a dot, a count - has its own name, so the tab reads as “EN, 1 field outdated”.
Tokens
From css/components/nav.css. It declares --redz-fade-left, --redz-fade-right on its root, never on rows or items.
It reads the theme's --redz-space-16, --redz-space-8, --redz-text-muted, --redz-text, --redz-warning, --redz-border, --redz-space-4, --redz-radius-8, --redz-bg-hover, --redz-space-12, --redz-radius-6, --redz-fs-12, --redz-bg, --redz-shadow-1.
From Bootstrap 5.3
| Bootstrap 5.3 | REDZ.UI | Change | Note |
|---|---|---|---|
nav-item | - | remove | links sit straight in the nav |
nav-tabs | nav-underline | rename | tabs are drawn as an underline |
tab-content | - | remove | panels are siblings with role="tabpanel", hidden when not selected |
tab-pane | - | remove | panels are siblings with role="tabpanel", hidden when not selected |
data-bs-toggle="tab" | role="tab" aria-controls="ID" | trigger | in a role="tablist"; the library selects and shows the panel |
data-bs-toggle="pill" | role="tab" aria-controls="ID" | trigger | in a role="tablist" |
data-bs-spy | data-scrollspy | trigger | on the nav or list group of the links, not on the scrolled box; the link to the section in view gets aria-current="location" |
bootstrap.Tab | tab.click() | review | the library selects on click and by the keys |
shown.bs.tab | - | review | watch aria-selected, or the tab's click |