Tabs

Tabs switch between related views in the same place and show one panel at a time. The same component is sometimes called Pills.

Installation

bash
npx @smicolon/fasla-ui@latest add tabs

Run init once per project before adding components. See Installation.

Tabs is built on Base UI, so it needs the @base-ui/react package. add installs it with the component, and so does the shadcn CLI.

Preview

Account

Make changes to your account here.

Variants

Three styles, set on TabsList so a group never mixes them. boxed, the default, sets the tabs in a muted track and raises the active tab into a pill on it; it suits toolbars and inline view switches. bordered underlines the active tab, for content navigation and dense layouts. lifted raises the active tab out of a baseline like a file folder, for page-level sections.

boxed
Make changes to your account here.
bordered
Make changes to your account here.
lifted
Make changes to your account here.

Sizes

Three sizes, also set on TabsList so every tab in a group matches. md is the default. Tabs are 28, 30 or 32px tall, and lg uses 16px text with 20px icons. The boxed track adds 4px around them.

sm
Make changes to your account here.
md
Make changes to your account here.
lg
Make changes to your account here.

States

The active tab follows value. Hover and focus are real CSS states rather than props, so tab into a group to see the focus ring. A disabled tab is greyed out, and you cannot select it.

You have two orders out for delivery.

You have two orders out for delivery.

You have two orders out for delivery.

Accessibility

  • Renders the tablist, tab and tabpanel roles. Each tab points to its panel with aria-controls while that panel is shown, and the panel takes its name from its tab through aria-labelledby.
  • Tab enters the group on the active tab. The arrow keys move to the previous or next tab and select it, wrapping at the ends. To select with Enter or Space instead, set activateOnFocus to false. Home and End jump to the first and last tab. The arrow keys also reach a disabled tab, so a screen reader announces it, but it cannot be selected.
  • Give TabsList an aria-label that names the group.
  • An icon before the label is decorative, so mark it aria-hidden and let the label name the tab.
  • Direction is inherited from dir, never a prop. In right-to-left pages the arrow keys follow the reading order, so the left arrow moves to the next tab.

Props

PropTypeDefaultDescription
valuestring—Active tab, when controlled.
defaultValuestring—Tab active at first. Without it or value, the first enabled tab is active.
onValueChange(value, eventDetails) => void—Called when the active tab changes. Call eventDetails.cancel() to refuse the change.
variant"boxed" | "bordered" | "lifted""boxed"How the active tab is shown. Set on TabsList.
size"sm" | "md" | "lg""md"Size of every tab in the group. Set on TabsList.
activateOnFocusbooleantrueWhether the arrow keys also select the tab they move to. Set on TabsList.
TabsTrigger{ value, disabled }—A tab button; its value names its panel, and disabled turns it off.
TabsContent{ value }—The panel shown for its value.
classNamestring—Extra classes for the part.

Usage

Basic tabs

tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"

export function AccountTabs() {
  return (
    <Tabs defaultValue="account">
      <TabsList aria-label="Account settings">
        <TabsTrigger value="account">Account</TabsTrigger>
        <TabsTrigger value="password">Password</TabsTrigger>
      </TabsList>
      <TabsContent value="account">Account content here.</TabsContent>
      <TabsContent value="password">Password content here.</TabsContent>
    </Tabs>
  )
}

Another style and size

tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"

export function BorderedAccountTabs() {
  return (
    <Tabs defaultValue="account">
      <TabsList variant="bordered" size="lg" aria-label="Account settings">
        <TabsTrigger value="account">Account</TabsTrigger>
        <TabsTrigger value="password">Password</TabsTrigger>
      </TabsList>
      <TabsContent value="account">Account content here.</TabsContent>
      <TabsContent value="password">Password content here.</TabsContent>
    </Tabs>
  )
}

With an icon

tsx
import { KeyRound, User } from "lucide-react"
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"

export function AccountTabsWithIcons() {
  return (
    <Tabs defaultValue="account">
      <TabsList aria-label="Account settings">
        <TabsTrigger value="account">
          <User aria-hidden="true" />
          Account
        </TabsTrigger>
        <TabsTrigger value="password">
          <KeyRound aria-hidden="true" />
          Password
        </TabsTrigger>
      </TabsList>
      <TabsContent value="account">Account content here.</TabsContent>
      <TabsContent value="password">Password content here.</TabsContent>
    </Tabs>
  )
}

Controlled tabs

tsx
"use client"

import { useState } from "react"
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"

export function ControlledAccountTabs() {
  const [value, setValue] = useState("account")

  return (
    <Tabs value={value} onValueChange={setValue}>
      <TabsList aria-label="Account settings">
        <TabsTrigger value="account">Account</TabsTrigger>
        <TabsTrigger value="password">Password</TabsTrigger>
      </TabsList>
      <TabsContent value="account">Account content here.</TabsContent>
      <TabsContent value="password">Password content here.</TabsContent>
    </Tabs>
  )
}
Change your password here.

Active tab: password