Skip to content

ComponentsNavigation

Tabs

Switch between related panels of content without leaving the page. Three styles (line, segmented, enclosed), two sizes, horizontal or vertical, with an animated selection indicator, keyboard navigation and support for tabs that are links.

React AriaSource
Production is healthy. The last deployment finished 12 minutes ago with no errors.

Installation

pnpm dlx shadcn@latest add @desyne/tabs

The CLI installs dependencies and any other components this one uses.

Usage

tsx
import { Tab, TabList, TabPanel, Tabs } from "@/components/ui/tabs";
tsx
<Tabs>
  <TabList aria-label="Account">
    <Tab id="profile">Profile</Tab>
    <Tab id="billing">Billing</Tab>
  </TabList>
  <TabPanel id="profile">…</TabPanel>
  <TabPanel id="billing">…</TabPanel>
</Tabs>

Each Tab is matched to the TabPanel with the same id. Only the selected panel is mounted. The first tab is selected by default; use defaultSelectedKey or selectedKey to change that.

Label the tab list

TabList renders role="tablist" and needs an accessible name. Pass aria-label, or aria-labelledby pointing at a visible heading.

When to use

  • Tabs — switch between peer views of the same object (a project's overview, deployments and settings) where only one view is needed at a time.
  • Toggle Button Group — pick a value or mode that changes content in place without panels, such as a chart's metric. Segmented tabs look similar; use tabs only when each option has its own panel.
  • Disclosure — sections that users may want open at the same time, or content that should stay in reading order.
  • Sidebar or Links — primary navigation between pages. Tabs can be links (see Tabs as links), but keep them to a small set of sibling routes.

Anatomy

tsx
<Tabs>                    {/* state, orientation, variant and size */}
  <TabList>               {/* role="tablist" */}
    <Tab>                 {/* role="tab", or a link when href is set */}
      <Icon />            {/* optional, auto-sized */}
      Label
      <Badge />           {/* optional count or status */}
    </Tab>
  </TabList>
  <TabPanel />            {/* role="tabpanel", one per tab */}
</Tabs>
PartRendersNotes
Tabs<div>Root. Holds selection state and passes variant and size to its parts through context. A column when horizontal, a row when vertical.
TabList<div role="tablist">The row (or column) of tabs. One tab stop; arrow keys move between tabs.
Tab<div role="tab"> or <a>A tab. Needs an id that matches a panel. Renders a link when given href.
Selection indicatorSelectionIndicatorRendered inside every tab. Only the selected tab's indicator shows; it slides between tabs. Hidden for enclosed.
TabPanel<div role="tabpanel">Content for the tab with the same id. Labelled by its tab.

Examples

Segmented

A pill-shaped track with a raised thumb. Suits short, mutually exclusive views such as billing periods or chart ranges, and fits inside toolbars and card headers.

$12 per seat, billed monthly.

Enclosed

Folder-style tabs that join the panel below. Give the panel a border without a top edge (border border-t-0) and remove the root gap (gap-0) so they connect. Good for editors and code samples.

export default function Page() {
  return <h1>Hello, world</h1>;
}

Sizes

md (default) and sm for every variant. Use sm in dense surfaces like card headers, toolbars and side panels.

With icons

Put a Lucide icon before the label. Icons are sized to 16px automatically unless you set a size-* class.

Your name, avatar and public profile.

Icon only

Tabs without visible text need an aria-label on each Tab. A small segmented list works well as a view switcher.

With badges

Counts and statuses are plain children. Use a small Badge so the tab height doesn't change.

24 open pull requests.

Scrollable

Tabs don't wrap or shrink. A horizontal TabList watches its size, and when the tabs are wider than the space it has (many tabs, long labels, small screens) it gets data-overflowing and scrolls horizontally, with no vertical scrollbar and the indicator kept inside the scroll area. The selected tab is scrolled into view on mount and whenever the selection changes; tabs reached with the arrow keys scroll into view as they get focus. Pass hideScrollbar to hide the (thin) scrollbar; the list still scrolls with touch, a trackpad, Shift+wheel and the arrow keys.

The list is capped at its container's width (max-w-full, min-w-0), so this also works when the TabList sits in a flex row next to other controls. Lists that fit are left as they are.

Security settings for the Acme workspace.

Vertical

orientation="vertical" stacks the tabs beside the panel, and adds up and down arrow navigation. Give the TabList a fixed width. Useful for settings pages with many sections.

General workspace settings.

Disabled

Disable individual tabs with disabledKeys on Tabs or isDisabled on a Tab, or every tab with isDisabled on Tabs. Disabled tabs are skipped by the keyboard. Tell users why a tab is unavailable, for example with an icon or a note in the page.

12 members across 3 teams.

Controlled

Own the selection with selectedKey and onSelectionChange when something outside the tabs needs to change it, such as Back and Continue buttons in a multi-step flow.

Name your project and pick a region.

Manual activation

By default, moving focus with the arrow keys selects the tab immediately. With keyboardActivation="manual", arrows only move focus and Enter or Space selects. Use this when rendering a panel is expensive, for example when it fetches data.

$184,200 this quarter across 1,240 invoices.

Dynamic tabs

Pass items to TabList with a render function, and map the same array to panels. Here a controlled selectedKey jumps to each new tab as it's added.

select count(*) from users where active;

Give each Tab an href to make it a real link (middle-click and "open in new tab" work). Selection then follows the URL: derive selectedKey from the current path, and wrap your app in React Aria's RouterProvider so clicks go through your client-side router. The preview uses a local stand-in for the router.

Rendered for #overview.

With the Next.js App Router:

app/providers.tsxtsx
"use client";

import { useRouter } from "next/navigation";
import { RouterProvider } from "react-aria-components";

export function Providers({ children }: { children: React.ReactNode }) {
  const router = useRouter();
  return <RouterProvider navigate={router.push}>{children}</RouterProvider>;
}
app/team/tabs.tsxtsx
"use client";

import { usePathname } from "next/navigation";
import { Tab, TabList, Tabs } from "@/components/ui/tabs";

export function TeamTabs() {
  const pathname = usePathname();
  return (
    <Tabs selectedKey={pathname}>
      <TabList aria-label="Team">
        <Tab id="/team" href="/team">Overview</Tab>
        <Tab id="/team/activity" href="/team/activity">Activity</Tab>
        <Tab id="/team/members" href="/team/members">Members</Tab>
      </TabList>
    </Tabs>
  );
}

When the panels are separate routes, render only the TabList and let each page render its own content.

Recipes

Account settings

Line tabs as the header of a Card, with a form in each panel and shared actions in the footer. Pad the TabList with the card's px-(--card-px) so the tabs line up with the content.

Used for sign-in and receipts.

Install command

Small segmented tabs switch the package manager, with a copy button in the same bar. The TabList can sit inside any layout wrapper as long as it's within Tabs.

pnpm dlx shadcn@latest add @desyne/tabs

Card with date range

A segmented range picker in CardAction drives the chart below. Wrapping the header and content in Tabs keeps the list and panels connected.

Revenue
Gross volume, all products
$51,920+9.8%

Accessibility

  • Implements the WAI-ARIA tabs pattern: role="tablist", role="tab" with aria-selected, and role="tabpanel" labelled by its tab through aria-labelledby.
  • The tab list is a single tab stop. Focus lands on the selected tab; the arrow keys move between tabs and wrap from the last to the first.
  • A panel is focusable (tabIndex="0") when it has no focusable content of its own, so keyboard users can reach and scroll it.
  • Disabled tabs are skipped by keyboard navigation.
  • Left and right arrows follow the reading direction and flip in right-to-left locales.
  • The selection indicator animation is disabled when the user prefers reduced motion.
  • Icon-only tabs need aria-label.

Keyboard

KeyAction
TabMoves focus to the selected tab, then into the panel
← / →Moves to the previous / next tab (both orientations)
↑ / ↓Moves to the previous / next tab (vertical only)
Home / EndMoves to the first / last tab
Enter / SpaceSelects the focused tab (with keyboardActivation="manual")

Styling

Data attributes

On Tabs and TabList:

AttributePresent when
data-orientationAlways, set to horizontal or vertical. Tabs adds group/tabs, so children can use group-data-[orientation=vertical]/tabs:….

On TabList only:

AttributePresent when
data-overflowingA horizontal list is wider than its box and scrolls

On each Tab:

AttributePresent when
data-selectedThe tab is selected
data-hoveredHovered with a mouse or pen
data-pressedBeing pressed
data-focused / data-focus-visibleFocused / focused with the keyboard
data-disabledThe tab is disabled

On each TabPanel:

AttributePresent when
data-focused / data-focus-visibleThe panel has focus / keyboard focus
data-inertMounted with shouldForceMount but not selected
data-entering / data-exitingThe panel is animating in / out

Slots

data-slotElement
tabsRoot
tab-listTab list
tabEach tab
tab-panelEach panel

Customizing

  • className on every part accepts a string or a function of its render props, and is merged with the built-in classes with tailwind-merge.
  • The tab label is wrapped in an inline <span> above the indicator, so icons and badges line up with the text.
  • The line indicator and selected text use the brand color; the segmented thumb uses card. Override with classes such as data-selected:text-foreground on Tab.
  • The root has gap-4 between the list and the panels. Change it with className="gap-0" (as in the enclosed examples) or any other gap.

API Reference

Tabs

Prop

Type

Also accepts every prop of React Aria's Tabs.

TabList

Prop

Type

Tab

Prop

Type

TabPanel

Prop

Type

  • Toggle Button Group — segmented control for a value, without panels.
  • Disclosure — sections that expand in place, several at once.
  • Sidebar — primary navigation for an app.
  • Card — a common container for tabbed content.