Gesso
Navigation

Tabs

In-page panels with an edge rule and an active indicator.

Tabs puts several panels of content in one place and shows one at a time, with a list of triggers to pick between them. The list is a 1px hairline on the edge that faces the panel, and the active trigger carries a 2px bg-primary mark on that same edge. It has no well, so for a sunken track of options that owns no panels, use Toggle group.

Name and email for this workspace.

Usage

import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';

<Tabs defaultValue="account">
  <TabsList>
    <TabsTrigger value="account">Account</TabsTrigger>
    <TabsTrigger value="password">Password</TabsTrigger>
  </TabsList>
  <TabsContent value="account">Account panel</TabsContent>
  <TabsContent value="password">Password panel</TabsContent>
</Tabs>

Give the root a defaultValue, or a value and onValueChange, so one tab is active from the start. Without one, no tab is active, no panel shows, and the mark is not drawn. Each TabsContent takes the value of its trigger, and only the active panel is mounted.

The mark

The list is h-10 and draws its hairline in bg-border along the bottom edge. The active trigger turns text-fg, and the mark sits on that hairline under it, 2px thick in bg-primary. When the active tab changes, the mark stretches toward the new tab. Its leading edge moves in --duration-fast and its trailing edge follows in --duration-base with --ease-out, so it looks like it reaches and then catches up. The first time it appears, and under reduced motion, it jumps instead. It re-measures when the list or a trigger resizes, so a window resize or a trigger that changes width keeps it in place.

Vertical

orientation="vertical" puts the list beside the panel and rotates the hairline and the mark to the inline-end edge, where the mark is 2px wide. Triggers fill the width of the list and align to the start, and the arrow keys change to Up and Down.

Name and email for this workspace.

<Tabs defaultValue="account" orientation="vertical">
  <TabsList>
    <TabsTrigger value="account">Account</TabsTrigger>
    <TabsTrigger value="password">Password</TabsTrigger>
  </TabsList>
  <TabsContent value="account">Account panel</TabsContent>
  <TabsContent value="password">Password panel</TabsContent>
</Tabs>

Groups

TabsGroup gathers a run of triggers under a label inside a vertical list, like the parts of a settings area that belong together. The label sits in the caption style and the group's triggers are indented under it. They are still triggers of the same list, so the arrow keys move through them in order with the rest, and the mark slides into the group like any other tab.

Campaign details

The people who sign, and what each signature says.

<TabsList>
  <TabsTrigger value="profile">Profile</TabsTrigger>
  <TabsGroup label="Campaign details">
    <TabsTrigger value="business">Your business</TabsTrigger>
    <TabsTrigger value="team">Team</TabsTrigger>
  </TabsGroup>
</TabsList>

Icons and disabled tabs

iconBefore on a trigger is the same slot as on Button and Chip. It takes an icon component and draws it 16px in text-primary, and the icon is hidden from assistive tech, so a trigger with only an icon needs an aria-label.

Name and email for this workspace.

disabled keeps a trigger in the list at reduced opacity. It cannot be clicked and the arrow keys skip it.

First panel.

Props

Tabs takes every prop of the Radix Tabs parts, such as value, defaultValue, onValueChange, orientation, dir, and activationMode on Tabs, loop on TabsList, and value, disabled, and forceMount on TabsTrigger and TabsContent, plus these.

PropTypeDefaultWhat it does
iconBeforeComponentType<{ className?: string }>noneOn TabsTrigger. Draws a 16px icon in text-primary before the label.
labelstringrequiredOn TabsGroup. The group's name, shown above its triggers and read as each one's description.

Accessibility

  • TabsList is a tablist with aria-orientation set from orientation. Each trigger is a tab with aria-selected and aria-controls pointing at its panel, and each panel is a tabpanel with aria-labelledby pointing back at its trigger.
  • The list is one tab stop, and Tab lands on the active tab. The arrow keys for the orientation, Left and Right or Up and Down, move between triggers and wrap at the ends, and Home and End jump to the first and last. Disabled triggers are skipped.
  • Activation is automatic, so moving focus to a tab with the arrow keys also shows its panel. Set activationMode="manual" on Tabs and the arrow keys only move focus, and Space or Enter shows the panel.
  • A panel is itself a tab stop, so Tab from the list goes into the panel, and it shows a 2px focus ring on :focus-visible. A trigger shows the same ring.
  • A trigger inside TabsGroup is described by the group's label through aria-describedby, so a screen reader says "Team, tab, Campaign details". The label itself is plain text, not a heading, since the list stays one set of tabs.
  • Triggers are at least 40px tall and the list is 40px tall in both orientations. There is no larger target on a touch screen.
  • Under reduced motion the mark jumps without stretching and the trigger color changes without a transition.
  • In forced colors the hairline and the mark are background fills, so they do not show. The active tab is still reported through aria-selected, but nothing on screen marks it apart from its text color, which the system may replace.

When to use something else

  • Toggle group when the options change the view you are already in and own no panels. Tabs gives each option a panel and the tab roles, and Toggle group only reports a value in a sunken well.
  • Tab switcher is the same well built from a tabs array, for two or three views or modes. It is not a tablist, so use Tabs when there are panels.
  • Sidebar when the places are the pages of the app, with groups, counts, and routes, rather than panels of one page.
  • Stepper when the steps are ordered and the reader moves through them in sequence.
  • Pagination when the panels are pages of one list.
  • Accordion when the sections are stacked and more than one may stay open.

Install

shadcn add @gesso/tabs

On this page