Tabs
Underlined tabs that switch between sections of a page.
import { Tabs } from "@/ui";Tabs matches the Figma Tabs component: three sizes, a regular and an extended length, and Default, Hover, Active and Disabled states. A tab can have an icon before or after its label, a count, or only an icon.
Unselected tabs have a 1px --color-grey-100 underline and --color-text-primary text, and turn blue on hover. The selected tab has a 2px --color-primary-500 underline and blue medium text. The underline is drawn inside the tab, so the height is the same in every state.
Basic tabs
Tabs are controlled. Keep activeIndex in state and update it in onTabChange; the event's index is the tab that was clicked. Only the selected tab's content is rendered, below the tab list.
const [active, setActive] = useState(0);
<Tabs
activeIndex={active}
onTabChange={(e) => setActive(e.index)}
tabData={[
{ label: "Overview", content: <CampaignOverview /> },
{ label: "Campaigns", content: <CampaignList /> },
{ label: "Pipeline", content: <PipelineSummary /> },
]}
/>Icons and counts
prefixIcon puts a Phosphor icon before the label and count adds a small count after it. The count is grey on unselected tabs and blue with white text on the selected tab. Icons take the text colour.
import { ChartBar, Megaphone, Funnel, Table } from "@phosphor-icons/react";
tabData={[
{ label: "Summary", prefixIcon: ChartBar },
{ label: "Campaigns", prefixIcon: Megaphone, count: 14 },
{ label: "Funnel", prefixIcon: Funnel },
{ label: "Raw data", prefixIcon: Table, count: 3 },
]}suffixIcon puts the icon after the label instead. Figma's examples show both at once, but use one icon per tab.
tabData={[
{ label: "All accounts", suffixIcon: CaretDown },
{ label: "Saved views", suffixIcon: CaretDown, count: 4 },
]}Pass icon components, not elements (ChartBar, not <ChartBar />). Tabs sets their size.
Sizes
<Tabs size="small" ... />
<Tabs size="medium" ... />
<Tabs size="large" ... />| Size | Text | Icon | Height |
|---|---|---|---|
small | 12px / 16px | 12px | 32px |
medium (default) | 12px / 19px | 12px | 35px |
large | 14px / 22px | 16px | 38px |
Every size has 8px top and bottom padding, 16px side padding and a 10px gap between the icon, label and count. The count is 16px tall with 12px text.
Figma's Small tabs and the count use 10px text. We use 12px, since no product text goes below 12px. Small tabs keep their 32px height.
Extended
extended doubles the side padding to 32px, for a short tab list that should take up more of the bar.
<Tabs extended activeIndex={active} onTabChange={(e) => setActive(e.index)} tabData={tabData} />Icon only
Set iconOnly on a tab to show its icon without the label. The label is still required: it becomes the tab's accessible name and its tooltip.
tabData={[
{ label: "Chart", prefixIcon: ChartBar, iconOnly: true },
{ label: "Table", prefixIcon: Table, iconOnly: true },
{ label: "List", prefixIcon: ListBullets, iconOnly: true, count: 2 },
]}An icon-only tab is 44px wide at small and medium and 48px at large. Use it only when the icons are unambiguous, like a chart and table switch.
Disabled
Set disabled on one tab to stop it from being picked, or on Tabs to disable every tab. Disabled tabs have --color-text-disabled text and a grey-50 count, and can't be clicked or reached with the arrow keys. If the selected tab is disabled, its underline turns --color-grey-300.
tabData={[
{ label: "Overview" },
{ label: "Campaigns", count: 14 },
{ label: "Settings", prefixIcon: Gear, disabled: true },
]}
<Tabs disabled ... />Navigation tabs
When the tabs switch routes rather than panels, leave out content and navigate in onTabChange, as in a page header.
<Tabs
activeIndex={isWorkstreams ? 1 : 0}
onTabChange={(e) => navigate(e.index === 0 ? "/home" : "/home/workstreams")}
tabData={[{ label: "Home" }, { label: "Workstreams" }]}
size="large"
navClassName="px-8 h-[52px]"
showDefaultBottomBorder={false}
/>showDefaultBottomBorder={false} removes the grey underline from unselected tabs, which suits a header that already has its own bottom border. Tabs stretch to the height of the tab list, so the underline sits on the header's edge.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
tabData | Tab[] | [] | Nothing renders when it is empty. |
activeIndex | number | null | 0 | null selects no tab, for a page that belongs to none of them. |
onTabChange | (e: { originalEvent, index }) => void | ||
size | "small" | "medium" | "large" | "medium" | |
extended | boolean | false | 32px side padding. |
disabled | boolean | false | Disables every tab. |
showDefaultBottomBorder | boolean | true | Grey underline on unselected tabs. |
navClassName | string | Classes for the tab list. | |
headerClassName | string | Classes for each tab. | |
className | string | Classes for the root. Other props are passed to the root too. |
Each Tab:
| Field | Type | Notes |
|---|---|---|
label | string | Required, also for icon-only tabs. |
content | ReactNode | Shown below the list when the tab is selected. |
prefixIcon | ElementType | Icon before the label. |
suffixIcon | ElementType | Icon after the label. |
count | number | string | Count after the label. |
iconOnly | boolean | Hides the label. Needs prefixIcon. |
disabled | boolean |
Differences from Figma
- Small tab and count text is 12px, not 10px.
- In Figma, the Hover and Disabled tabs have no grey underline. Here they keep it, so the line under a tab list doesn't break when you hover a tab.
- Figma's disabled Small tab without a count has 16px icons. Here every Small tab has 12px icons, as in the rest of the Small set.
- Figma has no disabled selected tab. Here its underline is grey-300.
- Icon-only Small and Medium tabs are 28px tall in Figma, shorter than the 32px and 35px text tabs. Don't mix icon-only and text tabs in one list.
Accessibility
- The list is
role="tablist"and each tab is abuttonwithrole="tab"andaria-selected. When tabs have content, the panel isrole="tabpanel"and labelled by its tab. - Tab moves focus to the selected tab. Left and Right arrows move between tabs, Home and End jump to the first and last, and Enter or Space selects (manual activation, so arrowing through route tabs doesn't navigate).
- Disabled tabs use the
disabledattribute, so they are skipped by the keyboard and announced as unavailable. - Icon-only tabs use
labelas theiraria-label.
Do and don't
- Do use Tabs for two to five peer sections of one page or one object, such as a campaign's Overview, Creatives and Pipeline.
- Do keep labels to one or two words in sentence case.
- Do use a count when it helps someone pick a tab, like the number of items in each.
- Don't use Tabs for a small switch inside a card or panel. Use TabToggle there.
- Don't put actions such as "New campaign" in the tab list. Put them in the page header.