Skip to content
TopClass UI

Tabs

Views of one thing, shown one at a time. Every panel renders on the server and stays in the HTML, so hidden text is still indexed.

split - size not measured yet

Tabs

split - size not measured yet

Views of one thing, shown one at a time. Every panel renders on the server and stays in the HTML, so hidden text is still indexed.

Install
Revenue is up 12% against last quarter.

Try it

variant
size
keepMounted

The code

tabs.tsx
import { TabsList, TabsPanel, TabsRoot, TabsTab } from 'topclassui/tabs'

<TabsRoot defaultValue="overview">
  <TabsList aria-label="Project">
    <TabsTab value="overview">Overview</TabsTab>
    <TabsTab value="activity">Activity</TabsTab>
    <TabsTab value="settings">Settings</TabsTab>
  </TabsList>
  <TabsPanel value="overview">Revenue is up 12% against last quarter.</TabsPanel>
  <TabsPanel value="activity">Three people edited this project today.</TabsPanel>
  <TabsPanel value="settings">Only an owner can rename or delete this project.</TabsPanel>
</TabsRoot>

keepMounted is true here, where Base UI's default is false: inactive panels stay in the server HTML, hidden.

Every state

2 states
The active tab is semibold over a bar of the primary colour.
The active tab sits on a raised surface.

Import from the package

topclassui/tabs
import { TabsRoot, TabsList, TabsPanel, TabsTab } from 'topclassui/tabs'

Install once:npm i topclassui@beta zod

Examples

  • Four flat parts and no bare Tabs: TabsRoot, TabsList, TabsTab and TabsPanel. Each passes the rest of its props to the element it draws.
  • Always pass defaultValue, or value. Without it the server HTML shows no panel until the page hydrates.
  • Write a panel's content in your Server Component. A heavy panel can take keepMounted={false}, which leaves it out until its tab opens.
  • variant on TabsRoot: line draws a bar under the active tab, enclosed raises it inside a tinted box.
  • size on TabsRoot sets every tab. custom sets nothing: give each TabsTab its own className.
  • Many tabs scroll sideways inside the list, and the page does not. A long label ends in an ellipsis and is cut at 100 characters.
  • value with onValueChange needs a 'use client' file, as the controlled example shows.
basic
Revenue is up 12% against last quarter.
enclosed
Revenue is up 12% against last quarter.
sizes
The xs tabs of the line look.
The sm tabs of the line look.
The md tabs of the line look.
The lg tabs of the line look.
The xl tabs of the line look.
Tabs sized by their own classes.
The xs tabs of the enclosed look.
The sm tabs of the enclosed look.
The md tabs of the enclosed look.
The lg tabs of the enclosed look.
The xl tabs of the enclosed look.
Tabs sized by their own classes.
disabled
Version 2.4 ships on Monday.
Version 2.4 ships on Monday.
server-content
controlled
Only you can see a draft.
many
Everything under Overview.
Everything under Overview.
long-label
Quarterly revenue, costs and forecast for every region the finance team reports on.
Quarterly revenue, costs and forecast for every region the finance team reports on.
In Arabic, right to left
ارتفعت الإيرادات 12% عن الربع الماضي.

Props

TabsRoot

The props of TabsRoot
PropTypeDefaultLimitsDescription
sizexs | sm | md | lg | xl | custom"md"-TabsRoot: the height, padding and type step of every tab; custom emits none.
variantline | enclosed"line"-TabsRoot: line underlines the active tab, enclosed lifts it onto a raised surface.

TabsList

The props of TabsList
PropTypeDefaultLimitsDescription
aria-labelstring--The name read out when no visible text names the group.
aria-labelledbystring--The id of the visible text that names the group.

TabsPanel

The props of TabsPanel
PropTypeDefaultLimitsDescription
keepMountedbooleantrue-TabsPanel: whether a panel that is not active stays in the HTML, hidden.

TabsTab

No props of its own: it takes the attributes of the element it renders.

Keyboard and accessibility

The keyboard of Tabs
KeysWhat it does
ArrowRightMoves to the next tab; in RTL, the tab before.
ArrowLeftMoves to the tab before; in RTL, the next one.
HomeMoves to the first tab.
EndMoves to the last tab.
EnterOpens the focused tab.
SpaceOpens the focused tab.
TabLeaves the list for the open panel.
  • TabsList needs aria-label or aria-labelledby: TypeScript refuses it without one.
  • A disabled tab cannot be opened, but the arrow keys still stop on it, so a screen reader hears that it is there.
  • The panel is a tab stop of its own. Pass tabIndex={-1} to a TabsPanel whose first thing is a button or a field.

Right to left

The first tab sits at the right. With TopClassProvider direction="rtl" the left arrow goes to the next tab; without a provider the keys stay left to right.

Server and client

Renders on the server; the interactive part is a client component that ships only when it is used.

Files: index.tsx, client.tsx.

On the chip: split - size not measured yet.