navigation
Tabs
Switch between views of the same subject, one panel at a time.
Rich widgetNeeds a script for its behavior.
Usage
A tab list of buttons and one panel per tab. The selected tab has aria-selected="true"; the other panels are hidden.
Tuesday at 09:00 with Sam, 90 minutes in the blue car.
Pick-up at the station, then the ring road and two highway exits.
Practise merging and keeping distance at higher speeds.
<div class="ca-tabs w-full max-w-lg">
<div class="ca-tabs__list" role="tablist" aria-label="Lesson details">
<button class="ca-tabs__tab" type="button" role="tab" id="tab-overview" aria-controls="panel-overview" aria-selected="true">Overview</button>
<button class="ca-tabs__tab" type="button" role="tab" id="tab-route" aria-controls="panel-route" aria-selected="false" tabindex="-1">Route</button>
<button class="ca-tabs__tab" type="button" role="tab" id="tab-notes" aria-controls="panel-notes" aria-selected="false" tabindex="-1">Notes</button>
</div>
<div class="ca-tabs__panel" role="tabpanel" id="panel-overview" aria-labelledby="tab-overview" tabindex="0">
<p>Tuesday at 09:00 with Sam, 90 minutes in the blue car.</p>
</div>
<div class="ca-tabs__panel" role="tabpanel" id="panel-route" aria-labelledby="tab-route" tabindex="0" hidden>
<p>Pick-up at the station, then the ring road and two highway exits.</p>
</div>
<div class="ca-tabs__panel" role="tabpanel" id="panel-notes" aria-labelledby="tab-notes" tabindex="0" hidden>
<p>Practise merging and keeping distance at higher speeds.</p>
</div>
</div>Examples
Unavailable tab
A disabled tab stays visible but cannot be selected; the arrow keys skip it.
Invoice 2026-114 for ten lessons, sent on 3 November.
Paid in full on 9 November.
No refund requested.
<div class="ca-tabs w-full max-w-lg">
<div class="ca-tabs__list" role="tablist" aria-label="Invoice">
<button class="ca-tabs__tab" type="button" role="tab" id="tab-invoice" aria-controls="panel-invoice" aria-selected="true">Invoice</button>
<button class="ca-tabs__tab" type="button" role="tab" id="tab-payments" aria-controls="panel-payments" aria-selected="false" tabindex="-1">Payments</button>
<button class="ca-tabs__tab" type="button" role="tab" id="tab-refund" aria-controls="panel-refund" aria-selected="false" tabindex="-1" disabled>Refund</button>
</div>
<div class="ca-tabs__panel" role="tabpanel" id="panel-invoice" aria-labelledby="tab-invoice" tabindex="0">
<p>Invoice 2026-114 for ten lessons, sent on 3 November.</p>
</div>
<div class="ca-tabs__panel" role="tabpanel" id="panel-payments" aria-labelledby="tab-payments" tabindex="0" hidden>
<p>Paid in full on 9 November.</p>
</div>
<div class="ca-tabs__panel" role="tabpanel" id="panel-refund" aria-labelledby="tab-refund" tabindex="0" hidden>
<p>No refund requested.</p>
</div>
</div>Guidance and accessibility
When to use
- Use tabs for a few views of the same subject that people switch between, such as the overview, route and notes of one lesson.
- Keep the labels short, and the number of tabs small enough to see at once.
- Do not use tabs for steps in a process or for navigating to other pages; use links for those.
Accessibility
- Tabs follow the WAI-ARIA tabs pattern.
ca-tabs.jshandles the keyboard: the arrow keys move between tabs (mirrored in right-to-left), Home and End jump to the first and last, and a tab opens its panel as soon as it is focused. - Only the selected tab is in the tab order (
tabindex="0"); Tab then moves into the panel, which hastabindex="0"so it can take focus. - Give the tab list an
aria-label, and each panelaria-labelledbypointing at its tab. - Without the script, only the panel that is not
hiddenshows. Loadca-tabs.json every page with tabs.
Classes
Generated from the component contract. Parts use __, modifiers use --.
| Class | Kind | Use it for |
|---|---|---|
ca-tabs | Component | Wrapper around the tab list and the panels. |
ca-tabs__list | Part | The role="tablist" row; it scrolls sideways when the tabs do not fit. |
ca-tabs__tab | Part | A role="tab" button. aria-selected="true" marks the selected one. |
ca-tabs__panel | Part | A role="tabpanel" with the content of one tab. |
Skins
The same markup, dressed by each skin. Structure and behavior stay the same; only the look changes.
Base skin
Neutral grayscale starting point; product skins extend it and change what differs.
Tuesday at 09:00 with Sam, 90 minutes in the blue car.
Pick-up at the station, then the ring road and two highway exits.
Practise merging and keeping distance at higher speeds.
Calaca skin
The Calaca UI brand: hueso paper and tinta ink, cempasúchil marigold, rosa and jade.
Tuesday at 09:00 with Sam, 90 minutes in the blue car.
Pick-up at the station, then the ring road and two highway exits.
Practise merging and keeping distance at higher speeds.
Proof skin
Pilot skin pushed far from base: square, heavy, offset shadows, larger targets.
Tuesday at 09:00 with Sam, 90 minutes in the blue car.
Pick-up at the station, then the ring road and two highway exits.
Practise merging and keeping distance at higher speeds.
Crossview skin
The CrossView proofing table: cool ground, bond-paper sheets, rule-grey hairlines, one red mark.
Tuesday at 09:00 with Sam, 90 minutes in the blue car.
Pick-up at the station, then the ring road and two highway exits.
Practise merging and keeping distance at higher speeds.
Framework notes
Link the prebuilt stylesheet of your skin next to your brand theme. No build step.
<link rel="stylesheet" href="theme.css" />
<link rel="stylesheet" href="ca-base.css" />In Astro, Vite and other Tailwind projects, import Calaca after Tailwind and your theme in your main stylesheet.
@import "tailwindcss";
@import "./theme.css";
@import "@calaca/ui/core.css";
@import "@calaca/ui/skins/base.css";Same stylesheet as above; write the classes in className.
<button className="ca-button ca-button--primary ca-button--md" type="button">Save changes</button>Load the tabs script once per page; it handles every tab list, also ones a framework adds later. In Astro or Next.js, import @calaca/ui/behavior/tabs once in the layout, as with the commands polyfill.
<script type="module" src="ca-tabs.js"></script>