Tabs
Switch between panels in one place. Arrow keys, Home and End select a tab.
Usage
The default is a segmented bar. The selected tab has a white
background and a border. Each tab points to its panel with
aria-controls; start the other panels
hidden. Tabs work as soon as they sit inside
.tabs.
Account
Edit the account name and email. Save when done.
Password
Change your password. You will need to sign in again after saving.
<div class="tabs" id="tb-use">
<div role="tablist" aria-label="Example">
<button type="button" role="tab" id="tb-use-t1" aria-selected="true" aria-controls="tb-use-p1">Account</button>
<button type="button" role="tab" id="tb-use-t2" aria-selected="false" aria-controls="tb-use-p2" tabindex="-1">Password</button>
</div>
<div role="tabpanel" id="tb-use-p1" aria-labelledby="tb-use-t1" tabindex="0">
<div class="card"><header><h3>Account</h3><p>Edit the account name and email. Save when done.</p></header></div>
</div>
<div role="tabpanel" id="tb-use-p2" aria-labelledby="tb-use-t2" tabindex="0" hidden>
<div class="card"><header><h3>Password</h3><p>Change your password. You will need to sign in again after saving.</p></header></div>
</div>
</div>
Underlined tabs
For page-level tabs under a heading. Set
data-variant="line" on the tablist.
Overview
Orders, sales and returns for the selected period.
Analytics
Where visitors come from and what they buy.
Reports
Saved reports, ready to open or export.
<div class="tabs" id="tb-line">
<div role="tablist" aria-label="Example" data-variant="line">
<button type="button" role="tab" id="tb-line-t1" aria-selected="true" aria-controls="tb-line-p1">Overview</button>
<button type="button" role="tab" id="tb-line-t2" aria-selected="false" aria-controls="tb-line-p2" tabindex="-1">Analytics</button>
<button type="button" role="tab" id="tb-line-t3" aria-selected="false" aria-controls="tb-line-p3" tabindex="-1">Reports</button>
</div>
<div role="tabpanel" id="tb-line-p1" aria-labelledby="tb-line-t1" tabindex="0">
<div class="card"><header><h3>Overview</h3><p>Orders, sales and returns for the selected period.</p></header></div>
</div>
<div role="tabpanel" id="tb-line-p2" aria-labelledby="tb-line-t2" tabindex="0" hidden>
<div class="card"><header><h3>Analytics</h3><p>Where visitors come from and what they buy.</p></header></div>
</div>
<div role="tabpanel" id="tb-line-p3" aria-labelledby="tb-line-t3" tabindex="0" hidden>
<div class="card"><header><h3>Reports</h3><p>Saved reports, ready to open or export.</p></header></div>
</div>
</div>
Small, for a card header
Product content
100% cotton bed sheet
Drap en coton 100 %
<div class="card tabs">
<header class="page-header">
<div><h3>Product content</h3></div>
<div role="tablist" aria-label="Language" data-size="sm">
<button type="button" role="tab" id="tb-sm-t1" aria-selected="true" aria-controls="tb-sm-p1">EN</button>
<button type="button" role="tab" id="tb-sm-t2" aria-selected="false" aria-controls="tb-sm-p2" tabindex="-1">FR</button>
</div>
</header>
<p role="tabpanel" id="tb-sm-p1" aria-labelledby="tb-sm-t1">100% cotton bed sheet</p>
<p role="tabpanel" id="tb-sm-p2" aria-labelledby="tb-sm-t2" hidden>Drap en coton 100 %</p>
</div>
Vertical tabs beside the panels
Set aria-orientation="vertical". Arrow Up / Down
replace Left / Right. Below 40rem the tabs sit above the panel.
General
Store name, address and time zone.
Checkout
What customers fill in at checkout.
Notifications
Emails the store sends to customers.
<div class="tabs" id="tb-vert">
<div role="tablist" aria-label="Example" aria-orientation="vertical">
<button type="button" role="tab" id="tb-vert-t1" aria-selected="true" aria-controls="tb-vert-p1">General</button>
<button type="button" role="tab" id="tb-vert-t2" aria-selected="false" aria-controls="tb-vert-p2" tabindex="-1">Checkout</button>
<button type="button" role="tab" id="tb-vert-t3" aria-selected="false" aria-controls="tb-vert-p3" tabindex="-1">Notifications</button>
</div>
<div role="tabpanel" id="tb-vert-p1" aria-labelledby="tb-vert-t1" tabindex="0">
<div class="card"><header><h3>General</h3><p>Store name, address and time zone.</p></header></div>
</div>
<div role="tabpanel" id="tb-vert-p2" aria-labelledby="tb-vert-t2" tabindex="0" hidden>
<div class="card"><header><h3>Checkout</h3><p>What customers fill in at checkout.</p></header></div>
</div>
<div role="tabpanel" id="tb-vert-p3" aria-labelledby="tb-vert-t3" tabindex="0" hidden>
<div class="card"><header><h3>Notifications</h3><p>Emails the store sends to customers.</p></header></div>
</div>
</div>
Vertical, with underlined tabs
General
Store name, address and time zone.
Checkout
What customers fill in at checkout.
Notifications
Emails the store sends to customers.
<div class="tabs" id="tb-vline">
<div role="tablist" aria-label="Example" aria-orientation="vertical" data-variant="line">
<button type="button" role="tab" id="tb-vline-t1" aria-selected="true" aria-controls="tb-vline-p1">General</button>
<button type="button" role="tab" id="tb-vline-t2" aria-selected="false" aria-controls="tb-vline-p2" tabindex="-1">Checkout</button>
<button type="button" role="tab" id="tb-vline-t3" aria-selected="false" aria-controls="tb-vline-p3" tabindex="-1">Notifications</button>
</div>
<div role="tabpanel" id="tb-vline-p1" aria-labelledby="tb-vline-t1" tabindex="0">
<div class="card"><header><h3>General</h3><p>Store name, address and time zone.</p></header></div>
</div>
<div role="tabpanel" id="tb-vline-p2" aria-labelledby="tb-vline-t2" tabindex="0" hidden>
<div class="card"><header><h3>Checkout</h3><p>What customers fill in at checkout.</p></header></div>
</div>
<div role="tabpanel" id="tb-vline-p3" aria-labelledby="tb-vline-t3" tabindex="0" hidden>
<div class="card"><header><h3>Notifications</h3><p>Emails the store sends to customers.</p></header></div>
</div>
</div>
With icons
Account
Name, email and profile photo.
Security
Password and two-factor authentication.
Billing
Plan and invoices.
<div class="tabs" id="tb-icon">
<div role="tablist" aria-label="Example">
<button type="button" role="tab" id="tb-icon-t1" aria-selected="true" aria-controls="tb-icon-p1"><i class="icon icon-user" aria-hidden="true"></i>Account</button>
<button type="button" role="tab" id="tb-icon-t2" aria-selected="false" aria-controls="tb-icon-p2" tabindex="-1"><i class="icon icon-lock" aria-hidden="true"></i>Security</button>
<button type="button" role="tab" id="tb-icon-t3" aria-selected="false" aria-controls="tb-icon-p3" tabindex="-1"><i class="icon icon-credit-card" aria-hidden="true"></i>Billing</button>
</div>
<div role="tabpanel" id="tb-icon-p1" aria-labelledby="tb-icon-t1" tabindex="0">
<div class="card"><header><h3>Account</h3><p>Name, email and profile photo.</p></header></div>
</div>
<div role="tabpanel" id="tb-icon-p2" aria-labelledby="tb-icon-t2" tabindex="0" hidden>
<div class="card"><header><h3>Security</h3><p>Password and two-factor authentication.</p></header></div>
</div>
<div role="tabpanel" id="tb-icon-p3" aria-labelledby="tb-icon-t3" tabindex="0" hidden>
<div class="card"><header><h3>Billing</h3><p>Plan and invoices.</p></header></div>
</div>
</div>
A disabled tab cannot be selected, and the arrow keys skip it
Overview
Orders, sales and returns for the selected period.
Analytics
Where visitors come from and what they buy.
Reports
Saved reports, ready to open or export.
<div class="tabs" id="tb-dis">
<div role="tablist" aria-label="Example">
<button type="button" role="tab" id="tb-dis-t1" aria-selected="true" aria-controls="tb-dis-p1">Overview</button>
<button type="button" role="tab" id="tb-dis-t2" aria-selected="false" aria-controls="tb-dis-p2" tabindex="-1" disabled>Analytics</button>
<button type="button" role="tab" id="tb-dis-t3" aria-selected="false" aria-controls="tb-dis-p3" tabindex="-1">Reports</button>
</div>
<div role="tabpanel" id="tb-dis-p1" aria-labelledby="tb-dis-t1" tabindex="0">
<div class="card"><header><h3>Overview</h3><p>Orders, sales and returns for the selected period.</p></header></div>
</div>
<div role="tabpanel" id="tb-dis-p2" aria-labelledby="tb-dis-t2" tabindex="0" hidden>
<div class="card"><header><h3>Analytics</h3><p>Where visitors come from and what they buy.</p></header></div>
</div>
<div role="tabpanel" id="tb-dis-p3" aria-labelledby="tb-dis-t3" tabindex="0" hidden>
<div class="card"><header><h3>Reports</h3><p>Saved reports, ready to open or export.</p></header></div>
</div>
</div>
A tab can be a dropdown: choosing an entry selects the tab
Put popovertarget on the tab, pointing to a
.dropdown. Clicking the tab opens the menu; clicking
an entry selects the tab (add
popovertargetaction="hide" to close the menu).
Overview
All customers in the selected period.
By UTM
Customers split by the selected UTM value.
By product
Customers split by the product they bought.
<div class="tabs" id="tb-dd">
<div role="tablist" aria-label="Example">
<button type="button" role="tab" id="tb-dd-t1" aria-selected="true" aria-controls="tb-dd-p1">Overview</button>
<button type="button" role="tab" id="tb-dd-t2" aria-selected="false" aria-controls="tb-dd-p2" tabindex="-1" popovertarget="tb-dd-menu">By UTM<i class="icon icon-chevron-down" aria-hidden="true"></i></button>
<button type="button" role="tab" id="tb-dd-t3" aria-selected="false" aria-controls="tb-dd-p3" tabindex="-1">By product</button>
</div>
<div class="dropdown" id="tb-dd-menu" popover>
<button type="button" popovertarget="tb-dd-menu" popovertargetaction="hide">UTM Source</button>
<button type="button" popovertarget="tb-dd-menu" popovertargetaction="hide">UTM Medium</button>
<button type="button" popovertarget="tb-dd-menu" popovertargetaction="hide">UTM Campaign</button>
</div>
<div role="tabpanel" id="tb-dd-p1" aria-labelledby="tb-dd-t1" tabindex="0">
<div class="card"><header><h3>Overview</h3><p>All customers in the selected period.</p></header></div>
</div>
<div role="tabpanel" id="tb-dd-p2" aria-labelledby="tb-dd-t2" tabindex="0" hidden>
<div class="card"><header><h3>By UTM</h3><p>Customers split by the selected UTM value.</p></header></div>
</div>
<div role="tabpanel" id="tb-dd-p3" aria-labelledby="tb-dd-t3" tabindex="0" hidden>
<div class="card"><header><h3>By product</h3><p>Customers split by the product they bought.</p></header></div>
</div>
</div>
The page selects a tab with select()
Overview
Orders, sales and returns for the selected period.
Analytics
Where visitors come from and what they buy.
Reports
Saved reports, ready to open or export.
<div class="stack">
<div class="tabs" id="tb-js">
<div role="tablist" aria-label="Example">
<button type="button" role="tab" id="tb-js-t1" aria-selected="true" aria-controls="tb-js-p1">
Overview
</button>
<button
type="button"
role="tab"
id="tb-js-t2"
aria-selected="false"
aria-controls="tb-js-p2"
tabindex="-1"
>
Analytics
</button>
<button
type="button"
role="tab"
id="tb-js-t3"
aria-selected="false"
aria-controls="tb-js-p3"
tabindex="-1"
>
Reports
</button>
</div>
<div role="tabpanel" id="tb-js-p1" aria-labelledby="tb-js-t1" tabindex="0">
<div class="card">
<header>
<h3>Overview</h3>
<p>Orders, sales and returns for the selected period.</p>
</header>
</div>
</div>
<div role="tabpanel" id="tb-js-p2" aria-labelledby="tb-js-t2" tabindex="0" hidden>
<div class="card">
<header>
<h3>Analytics</h3>
<p>Where visitors come from and what they buy.</p>
</header>
</div>
</div>
<div role="tabpanel" id="tb-js-p3" aria-labelledby="tb-js-t3" tabindex="0" hidden>
<div class="card">
<header>
<h3>Reports</h3>
<p>Saved reports, ready to open or export.</p>
</header>
</div>
</div>
</div>
<div>
<button type="button" class="btn" data-size="sm" id="tb-js-show">Show reports</button>
</div>
</div>
<script type="module">
document.getElementById("tb-js-show").addEventListener("click", () => {
tsunami.tabs.select(document.getElementById("tb-js-t3"));
});
</script>
An element with data-close inside a tab closes it, and so does Delete
Closing removes the tab and its panel. If it was selected, the next (or previous) tab is selected instead.
Order #1042
2 items. Paid, awaiting shipment.
Order #1043
1 item. Shipped on Monday.
Order #1044
3 items. Partially refunded.
<div class="tabs" id="tb-close">
<div role="tablist" aria-label="Open orders">
<button type="button" role="tab" id="tb-close-t1" aria-selected="true" aria-controls="tb-close-p1">Order #1042<i class="icon icon-x" data-close aria-hidden="true"></i></button>
<button type="button" role="tab" id="tb-close-t2" aria-selected="false" aria-controls="tb-close-p2" tabindex="-1">Order #1043<i class="icon icon-x" data-close aria-hidden="true"></i></button>
<button type="button" role="tab" id="tb-close-t3" aria-selected="false" aria-controls="tb-close-p3" tabindex="-1">Order #1044<i class="icon icon-x" data-close aria-hidden="true"></i></button>
</div>
<div role="tabpanel" id="tb-close-p1" aria-labelledby="tb-close-t1" tabindex="0">
<div class="card"><header><h3>Order #1042</h3><p>2 items. Paid, awaiting shipment.</p></header></div>
</div>
<div role="tabpanel" id="tb-close-p2" aria-labelledby="tb-close-t2" tabindex="0" hidden>
<div class="card"><header><h3>Order #1043</h3><p>1 item. Shipped on Monday.</p></header></div>
</div>
<div role="tabpanel" id="tb-close-p3" aria-labelledby="tb-close-t3" tabindex="0" hidden>
<div class="card"><header><h3>Order #1044</h3><p>3 items. Partially refunded.</p></header></div>
</div>
</div>
The page hears tsunami:closetab first, and keeps the tab with preventDefault()
Saved draft
This tab can be closed.
Unsaved draft
The page will not let this tab close.
Nothing closed yet.
<div class="stack">
<div class="tabs" id="tb-keep">
<div role="tablist" aria-label="Example">
<button
type="button"
role="tab"
id="tb-keep-t1"
aria-selected="true"
aria-controls="tb-keep-p1"
>
Saved draft<i class="icon icon-x" data-close aria-hidden="true"></i>
</button>
<button
type="button"
role="tab"
id="tb-keep-t2"
aria-selected="false"
aria-controls="tb-keep-p2"
tabindex="-1"
data-unsaved
>
Unsaved draft<i class="icon icon-x" data-close aria-hidden="true"></i>
</button>
</div>
<div role="tabpanel" id="tb-keep-p1" aria-labelledby="tb-keep-t1" tabindex="0">
<div class="card">
<header>
<h3>Saved draft</h3>
<p>This tab can be closed.</p>
</header>
</div>
</div>
<div role="tabpanel" id="tb-keep-p2" aria-labelledby="tb-keep-t2" tabindex="0" hidden>
<div class="card">
<header>
<h3>Unsaved draft</h3>
<p>The page will not let this tab close.</p>
</header>
</div>
</div>
</div>
<p id="tb-keep-log">Nothing closed yet.</p>
</div>
<script type="module">
document.getElementById("tb-keep").addEventListener("tsunami:closetab", (e) => {
const kept = e.detail.tab.hasAttribute("data-unsaved");
if (kept) e.preventDefault();
document.getElementById("tb-keep-log").textContent =
e.detail.tab.textContent.trim() + (kept ? " was kept open." : " was closed.");
});
</script>
API
Lives in js/tabs.js. It uses event delegation, so
tabs added later work too.
| Function | Arguments | Description |
|---|---|---|
tsunami.tabs.select(tab) |
tab: element [role="tab"] |
Selects the tab, shows its panel and hides the others. Does nothing on a disabled tab. |
tsunami.tabs.close(tab) |
tab: element [role="tab"] |
Fires tsunami:closetab first. If not
cancelled, removes the tab and panel and, if it was
selected, selects the next (or previous) tab. Returns the
replacement tab (or null), or
false if it was kept.
|
Arrow Left / Right |
keyboard | Previous / next tab, wrapping, skipping disabled tabs. Vertical tabs use Arrow Up / Down. |
Home / End |
keyboard | First / last tab |
Delete |
keyboard |
On a tab with [data-close]: closes the tab
and focuses its replacement
|
Tab |
keyboard | Only the selected tab is in the Tab order |
Events
| Event | Detail | Description |
|---|---|---|
tsunami:closetab |
{ tab, panel } |
Fired on the tab before it closes. Bubbles and is
cancelable:
preventDefault() keeps the tab.
|
Attributes
| Attribute / element | Values | Description |
|---|---|---|
.tabs |
div |
Wraps the tablist and panels (data-tabs works
too)
|
[role="tablist"] |
child | Row of tabs. Add an aria-label. |
data-variant |
unset (default) segmented · line underlined
|
On the tablist: tab style |
data-size |
unset (default) 32px · sm 28px |
On the tablist: size. sm for card headers.
|
aria-orientation |
horizontal (default) · vertical
|
On the tablist: vertical puts the tabs beside
the panels and uses Arrow Up / Down (the tablist must be a
direct child of .tabs)
|
[role="tab"] |
button |
A tab. Can hold an .icon. |
aria-controls |
panel id | On a tab: the panel it shows (required) |
aria-selected |
true · false |
On a tab: the selected tab. Put
tabindex="-1" on the others. JS keeps them
updated.
|
disabled / aria-disabled |
true |
On a tab: cannot be selected, skipped by the keyboard |
popovertarget |
.dropdown id |
On a tab: the tab is a dropdown. Click a menu entry to select the tab. |
[data-close] |
element in a tab | Closable tab. Click this element or press Delete to close it. |
[role="tabpanel"] |
element |
Content. Add aria-labelledby pointing to its
tab, and hidden on all but the selected one.
|