On this page
Tabs organise related content into parallel sections that users can switch between without leaving the page.
- Overview
- Rules and guidance
- Properties
- Styles
- Accessibility
When to use
- To present two or more distinct views of the same subject, where each view is complete on its own and the user only needs one at a time
- To reduce visual complexity on a page that contains a large amount of related content
- To let the user switch context without navigating to a separate page
When not to use
- To show sequential steps or a wizard flow - tabs communicate parallel content, not progression. Use a separate page per step instead.
- To compare content across sections - if the user needs to move back and forth between tabs to make a comparison, the content should be visible on screen at the same time. Use a Data Table or a Summary List instead.
- To show one or two sections - the overhead of a tab strip is not worth it for only two sections where a simple heading would do. Use an Accordion or plain headings instead.
- As primary navigation - tabs belong inside a page, not at the site or application level. Use the application's navigation system instead.
- When section labels are long or numerous - tabs that wrap to a second row or truncate labels are harder to scan. If the labels do not fit comfortably in a single row, use an Accordion instead.
Anatomy

- Tab list (the horizontal strip containing all tab buttons)
- Tab (an individual tab button; one is active at any time)
- Active indicator (the coloured bar at the bottom of the active tab)
- Tab panel (the content area associated with the active tab)
Keyboard support
Keyboard functions
- Left arrow: move to the previous tab and activate it
- Right arrow: move to the next tab and activate it
- Tab: move focus into the active tab panel
Basic usage
Edit live code
<Tabs> <TabList> <Tab>Tab 1</Tab> <Tab>Tab 2</Tab> <Tab>Tab 3</Tab> </TabList> <TabPanel> <P>Tab 1 Content</P> </TabPanel> <TabPanel> <P>Tab 2 Content</P> </TabPanel> <TabPanel> <P>Tab 3 Content</P> </TabPanel> </Tabs>
Variants and features
Default selected tab
By default the first tab is active. Use defaultSelected to open a different tab on first render, passing a zero-based index.
Edit live code
<Tabs defaultSelected={1}> <TabList> <Tab>Billing periods</Tab> <Tab>Direct Debit</Tab> <Tab>Supplies</Tab> </TabList> <TabPanel> <P>Billing periods content</P> </TabPanel> <TabPanel> <P>Direct Debit content - selected on first render</P> </TabPanel> <TabPanel> <P>Supplies content</P> </TabPanel> </Tabs>
Controlled tabs
Use value and onSelected together to control the active tab from outside the component. This is useful when the active tab needs to be driven by external state, such as a URL parameter or a parent component.
onSelected fires with the zero-based index of the newly active tab. It does not fire on initial render.
Edit live code
() => { // ignore - for live code example only. const Example = () => { const [selected, setSelected] = useState(0); return ( <Stack spaceBetween={3}> <Tabs value={selected} onSelected={setSelected}> <TabList> <Tab>Billing periods</Tab> <Tab>Direct Debit</Tab> <Tab>Supplies</Tab> </TabList> <TabPanel> <P>Billing periods content</P> </TabPanel> <TabPanel> <P>Direct Debit content</P> </TabPanel> <TabPanel> <P>Supplies content</P> </TabPanel> </Tabs> <SecondaryCTAButton onClick={() => setSelected(2)}> Jump to Supplies </SecondaryCTAButton> </Stack> ); } return <Example />; }
Rich tab labels
A Tab accepts any content, not just a text label. Keep labels short enough to stay scannable in a single row.
Edit live code
<Tabs> <TabList> <Tab> Account<br /> 123456789<br /> <Badge variant="success">Active</Badge> </Tab> <Tab> Account<br /> 234567890<br /> <Badge>Inactive</Badge> </Tab> </TabList> <TabPanel> <P>Details for account 123456789</P> </TabPanel> <TabPanel> <P>Details for account 234567890</P> </TabPanel> </Tabs>
Tab panel visibility
Inactive tab panels are hidden using display: none. They remain in the DOM, so their content is preserved across tab switches. There is no lazy rendering. All panel content mounts on initial render.
usage
- Tabs must not be used to represent sequential steps or a wizard flowTabs communicate parallel content. Sequential steps require separate pages or a stepped form pattern so users understand they must complete each step in order.Copy rule ID for tabs.parallel-content-only
- Tabs must not be used as the primary navigation for a site or applicationTabs are for switching between views within a page. Primary navigation belongs at the application shell level.Copy rule ID for tabs.no-primary-navigation
- Tabs must contain at least two tabsA single tab has no navigational purpose and adds visual noise without benefit.Copy rule ID for tabs.minimum-two
- Tabs should not be used when the user needs to compare content across sectionsSwitching between tabs to compare requires memory. If comparison is the goal, show the content side by side or in a table.Copy rule ID for tabs.no-comparison
placement
- Tabs should use the inline prop when placed inside a Card or other raised surfaceWithout inline, Tabs renders its own Card surface, creating a nested raised surface with doubled borders and padding.Copy rule ID for tabs.inline-inside-raised-surface
content
- Tab labels must be distinct from one another and unambiguous about what each panel containsUsers need to know which tab to open before they open it. Labels that are too similar or too vague force trial-and-error navigation.Copy rule ID for tabs.labels-mutually-distinct
- Tab must not use an icon as the sole content of a tab labelIcons without text labels are inaccessible to screen reader users and ambiguous to sighted users unfamiliar with the icon.Copy rule ID for tabs.no-icon-only-labels
- Tab label should be one or two wordsShort labels are faster to scan and are less likely to cause the tab strip to overflow at small viewport widths.Copy rule ID for tabs.label-length
- Tab label should not end with punctuationPunctuation adds no meaning to a navigation label and creates visual noise.Copy rule ID for tabs.no-terminal-punctuation
interaction
- Tabs will render with role=tablist on the list container, role=tab on each tab, and role=tabpanel on each panel. Do not override these rolesCopy rule ID for tabs.aria-roles
- Tab will have aria-selected="true" when active and aria-selected="false" when inactive. The component manages this automatically via TabListCopy rule ID for tabs.aria-selected
- TabPanel will have an aria-describedby attribute referencing the id of its associated Tab. The component wires this up automatically via generated idsCopy rule ID for tabs.aria-describedby
- Tabs will respond to ArrowLeft and ArrowRight keys to move between tabs. The component implements this keyboard handler internallyCopy rule ID for tabs.arrow-key-navigation
- TabList will keep only the active tab in the page tab sequence, using roving tabindex. The component manages tabIndex on each Tab automaticallyCopy rule ID for tabs.roving-tabindex
- Tabs will move focus to the newly activated tab when arrow keys are used. Focus and activation move together in the Nebula implementationCopy rule ID for tabs.focus-follows-arrow
composition
- Tabs must have a TabList as its first childThe component clones the first child as the TabList and the remaining children as TabPanels. A different first child will receive TabList props incorrectly.Copy rule ID for tabs.first-child-tablist
- Tabs must have the same number of TabPanel children as Tab children in the TabListThe component maps panels to tabs by index. A mismatch results in panels receiving incorrect aria-describedby associations or tabs with no associated panel.Copy rule ID for tabs.tabpanel-count-matches-tab-count
- Tab must not contain interactive elements such as buttons, links, or form controlsTabs themselves are interactive. Nesting interactive elements creates ambiguous keyboard behaviour and breaks the tab role.Copy rule ID for tabs.no-interactive-elements-in-tab-labels
- TabPanel should not contain navigation that drives the user to a different tabNavigation inside a panel breaks the tab model and makes the interaction unpredictable for keyboard and screen reader users.Copy rule ID for tabs.no-navigation-in-panel
Tabs
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
value | number | — | No | Zero-based index of the selected tab (controlled). When it changes, that tab is selected and focused. |
defaultSelected | number | 0 | No | Zero-based index of the tab that is selected initially (uncontrolled). |
inline | boolean | — | No | Has no effect. |
children | ReactNode | — | Yes | A `TabList` followed by one `TabPanel` for each tab, in the same order. |
onSelected | (i: number) => void | — | No | Called with the zero-based index of the newly selected tab when the selection changes, but not on first render. It is also called again if the function reference changes, so pass a stable callback (for example from `useCallback`). |
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
TabList
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
Tab
Also accepts all props from HTMLAttributes, LiHTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
active | boolean | — | No | Whether the tab is selected. Set automatically by `TabList`, so you don't need to set it. |
onClick | MouseEventHandler<HTMLLIElement> | — | No | Called after the tab is selected, in addition to selecting it, for example to send an analytics event. |
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
TabPanel
Also accepts all props from HTMLAttributes.
This component has no props of its own.
Styles
tab
Defined in Tabs/Tab.tsx
font-size: ${responsiveFontSizes.body[0]};
font-family: ${fonts.display};
font-weight: ${active ? fontWeights.bold : fontWeights.normal};
line-height: ${lineHeights.body};
padding: ${space[3]} ${space[4]};
color: ${colors.heading};
flex-grow: 0;
text-align: center;
background-color: ${colors.canvas};
position: relative;
overflow: hidden;
cursor: pointer;
${mediaQueries.smallAndUp} {
font-size: ${responsiveFontSizes.body[1]};
}
&:first-child {
border-top-left-radius: ${radii.standard};
}
&:before {
content: '';
position: absolute;
background-color: ${active ? colors.primary : 'transparent'};
height: 0.3rem;
bottom: 0;
left: 0;
right: 0;
}
&:focus-visible {
${focusRing}
position: relative;
z-index: 10;
}
&:hover {
background-color: ${active ? 'transparent' : colors.canvasMuted};
}
tab-panel
Defined in Tabs/TabPanel.tsx
padding: ${space[4]};
${mediaQueries.smallAndUp} {
padding: ${space[6]};
}
display: ${hidden ? 'none' : 'block'};
border-top-width: ${borderWidths.standard};
border-style: solid;
border-color: ${colors.borderMuted};
In brief
Tabs is a tab list with one panel for each tab. A screen reader announces each tab with its position and whether it is selected, and the panel is named by its tab.
What Nebula provides
- The tabs have the roles tablist, tab and tabpanel, and each tab reports whether it is selected with aria-selected.
- Each panel is named by its tab with aria-labelledby, and only the selected panel is shown.
- Only the selected tab is in the tab order. The left and right arrow keys move to the previous or next tab, select it and focus it.
- A tab has a visible focus ring.
Keyboard behaviour
Only the selected tab is reached with Tab. The left and right arrow keys select and focus the previous or next tab, and do not wrap past the first or last.
Focus order
- The selected tab
- The selected panel
Interactions
- Tab - Moves focus to the selected tab, then to its panel
Accessible design
Make each label stand for its whole panel
The tab label is the only name the panel has, and it is also what a screen reader reads for the tab itself. Use the content of the tab to say what the panel holds, as in "Billing periods", and not "Other".
WCAG: 2.4.6 Headings and Labels
Accessible development
Always pass one TabPanel for each Tab, in the same order
A panel is named by the tab in the same position. A missing or extra panel leaves a tab with the wrong panel, or a panel with no name.
WCAG: 4.1.2 Name, Role, Value
If a page has more than one set of tabs, name each TabList
Extra props, such as aria-label, are passed on to the tab list. Without a name a screen reader user cannot tell two lists apart.
WCAG: 4.1.2 Name, Role, Value
Avoid changing value from code unless the user asked for it
When value changes, that tab is selected and focused. Moving focus without a user action pulls them away from what they were doing.
Accessibility testing status
Reviewed on 9 October 2026 against v17.0.1.
| Test | Status | Tested with |
|---|---|---|
| Default state | Tested | axe-core via the Storybook test runner (CI) |
| Advanced states | Partially tested | axe-core via the Storybook test runner (CI) |
| Keyboard navigation | Partially tested | keyboard (reported by the owner) |
| Screen reader | Partially tested | VoiceOver with Chrome (reported by the owner) |
What these tests and statuses mean
The table records which checks were run on this component on its own. It is not a statement of WCAG conformance, and it does not cover how the component is used.
The tests
- Default state: The component as it first appears, before anything is clicked or changed.
- Advanced states: Every other state and variant: error, open, selected, each size and style, and hovering.
- Keyboard navigation: Whether every function can be reached and used with the keyboard alone, with a visible focus ring.
- Screen reader: Whether a screen reader announces what is on screen correctly: the name, the role and any state.
The statuses
- Tested: Every check in this row passed in an automated run that repeats on every pull request.
- Manually tested: Every check in this row was run once, by a person or with a tool they started, and all of them passed. It is not re-run on every change.
- Partially tested: Some checks in this row were run once and passed; the rest are still to do.
- Issue found: A check in this row failed. The ticket is shown beside the status.
- Not tested: No check in this row has been run yet.
- Not applicable: This row does not apply to the component, for the reason given beside it.