Tabs
StableA row of tabs that switch between views, with arrow-key movement and optional panels.
Also known as tab bar, tab list, tab view
Install with npm install @tattva/ui (setup), or copy the code from the kit in Downloads.
Underline with counts
function Example() {
const [tab, setTab] = useState("inbox");
return (
<Tabs
label="Mail folders"
value={tab}
onChange={setTab}
tabs={[
{ id: "inbox", label: "Inbox", count: 12 },
{ id: "sent", label: "Sent" },
{ id: "drafts", label: "Drafts", count: 3 },
{ id: "archive", label: "Archive", disabled: true },
]}
/>
);
}The default look. A count sits after the name. A disabled tab is skipped by the arrow keys.
When to use it
Switches between views that belong together on one page, such as Inbox, Sent and Drafts. The active tab is marked by more than colour, and keyboard users get one tab stop and arrow keys.
Made of: Tab list · Tab · Count · Panel
Use it for
- Views of the same kind of content, like folders or report sections.
- Tabs with a count, such as unread items.
- Showing only one panel at a time to keep a page short.
Not for
- Choosing a setting with two or three short options: use SegmentedControl.
- Content that readers should open and close in place: use Collapsible.
- A list of actions: use Menu.
Do and avoid
3 new invoices
Pick your dates
Do
- Keep the labels short, ideally one or two words.
- Use manual activation when showing a panel is slow or has a cost.
- Show a count only when it helps people choose, and keep it a plain number.
- Keep the same tab order every time the page loads.
Avoid
- Do not nest tabs inside tabs.
- Do not use tabs to move through steps in order. Show the steps as a list.
- Do not disable a tab without explaining why elsewhere on the page.
- Do not put actions in the tab list. Use a Menu or Buttons.
On a phone
- The row of tabs scrolls sideways when it is too wide, with no scrollbar, and the selected tab is scrolled into view.
- The row fades at an edge where more tabs are hidden.
- Each tab is 44px tall on touch screens and never wraps its label.
- The gap between underline tabs is smaller on a phone and grows from the sm breakpoint.
Properties
| Prop | Type | Default | Description |
|---|---|---|---|
| tabsrequired | { id: string; label: ReactNode; count?: number; disabled?: boolean; panel?: ReactNode }[] | none | The tabs in order. panel is drawn only while its tab is active. |
| valuerequired | string | none | Id of the active tab. The component is fully controlled. |
| onChangerequired | (id: string) => void | none | Called with the tab id when a tab is clicked, or reached by arrow keys in auto mode. |
| labelrequired | string | none | Accessible name of the tab list. |
| variant | "underline" | "pill" | "underline" | Underline for page sections, pill for a small switch. |
| activation | "auto" | "manual" | "auto" | auto picks a tab as soon as arrow keys reach it. manual only moves focus; Enter or Space picks it. |
| fill | boolean | none | Tabs share the full width instead of sizing to their label. |
| className | string | none | Classes for the outer wrapper. |
"Show code under the preview gives the code for the example on the stage."
In Figma
In Figma, one Tabs switches all of this from its properties panel, across 4 variants. Each property sets the React prop beside it, so design and code use the same names.
| Figma property | React | Values |
|---|---|---|
| Variant | variant | Underline, Pill |
| Fill | fill | On or off |
States
States this part has beyond its default look. Build each one your screen can reach. States set by a property are in the Code tab.
- selected
- The tab matching value gets a heavier underline (or a filled pill), stronger text and aria-selected. When value changes, the underline or pill slides to the new tab in --dur-base; the first placement does not slide. In forced colours the tab gets a highlight border or background instead.
- disabled
- A tab with disabled looks dimmed, ignores clicks and is skipped by the arrow keys.
- hover
- An unselected tab's text darkens under the pointer, and a pill tab gets a soft background.
- focus
- A visible focus ring appears on the focused tab. Only the selected tab is in the Tab order.
- overflow
- When the tabs are wider than the space, the list scrolls sideways without a scrollbar and keeps the selected tab in view.
Tokens
Change one of these and every part that uses it follows.
--fg--fg-muted--border--surface-hover--accent--on-accent--dur-fast--dur-base--ease-arrive--focus-ring--radius-control
Built in
- The row is a tab list with a name. Each tab is a button with role tab, and screen readers say whether it is selected.
- Only the active tab is in the Tab order. Left and Right Arrow move between tabs and wrap around, and Home and End jump to the first and last.
- Disabled tabs are skipped by the arrow keys.
- The active tab points to its panel with aria-controls. The panel has role tabpanel and is named by its tab.
- Only the active panel is drawn, so hidden panels are not in the page.
- On touch screens each tab is 44px tall.
- The active tab uses a heavier line or fill as well as colour.
- The line or fill slides to the new tab in --dur-base on --ease-arrive. It does not slide when it is first placed, and it moves at once when people turn off motion in their system settings.
- In forced-colors mode the sliding line or fill is hidden, and the active tab gets a highlight-coloured border (underline) or background (pill) instead.
What you need to do
- Give the tab list a label that says what the tabs choose between, such as "Mail folders".
- Keep tab names short and different from each other.
- Keep the value in your own state. If a tab opens a page, change the page address as well.
- If you draw the content yourself instead of passing panel, give your content role tabpanel and label it with the tab.
Built toward WCAG 2.2 AA and checked automatically; no independent audit yet. Read the accessibility guide