Components

An underline tab bar — a token-styled wrapper over Base UI's Tabs. Give Tabs a defaultValue (or value/onValueChange to control it) and compose TabsList + TabsTab for the bar and TabsPanel for each panel. The selected tab is underlined by a bg-primary indicator that slides between tabs.

Install

npx shadcn@latest add @alix/tabs

Preview

Team settings…

Usage

<Tabs defaultValue="team">
  <TabsList>
    <TabsTab value="team">Team</TabsTab>
    <TabsTab value="billing">Billing</TabsTab>
  </TabsList>
  <TabsPanel value="team">Team settings</TabsPanel>
  <TabsPanel value="billing">Billing settings</TabsPanel>
</Tabs>

Examples

01

Equal-width tab row

Let each trigger flex when the tab set is a compact switcher with similarly weighted views.

Engagement summary for the current week.
<Tabs defaultValue="summary">
  <TabsList className="w-full">
    <TabsTab value="summary" className="flex-1">Summary</TabsTab>
    <TabsTab value="people" className="flex-1">People</TabsTab>
    <TabsTab value="finance" className="flex-1">Finance</TabsTab>
  </TabsList>
  <TabsPanel value="summary">Engagement summary for the current week.</TabsPanel>
  <TabsPanel value="people">People allocation and senior coverage.</TabsPanel>
  <TabsPanel value="finance">Revenue, margin, and working capital.</TabsPanel>
</Tabs>
02

Controlled dashboard view

Control the selected value when adjacent UI needs to reflect the active tab.

Current viewpipeline
Six qualified opportunities are awaiting partner review.
const [view, setView] = useState("pipeline");

return (
  <div className="w-full max-w-lg space-y-3">
    <div className="flex items-center justify-between gap-4">
      <span className="text-sm font-medium text-foreground">Current view</span>
      <Badge variant="primary">{view}</Badge>
    </div>
    <Tabs value={view} onValueChange={setView}>
      <TabsList>
        <TabsTab value="pipeline">Pipeline</TabsTab>
        <TabsTab value="delivery">Delivery</TabsTab>
        <TabsTab value="risk">Risk</TabsTab>
      </TabsList>
      <TabsPanel value="pipeline">
        <div className="border border-border p-4 text-sm text-muted-foreground">
          Six qualified opportunities are awaiting partner review.
        </div>
      </TabsPanel>
      <TabsPanel value="delivery">
        <div className="border border-border p-4 text-sm text-muted-foreground">
          Four active engagements need weekly steering notes.
        </div>
      </TabsPanel>
      <TabsPanel value="risk">
        <div className="border border-border p-4 text-sm text-muted-foreground">
          Two workstreams are flagged for scope and timing risk.
        </div>
      </TabsPanel>
    </Tabs>
  </div>
);
03

Panel content switcher

Tabs can switch between a populated DataTable and an EmptyState while Panel keeps one frame.

Engagement queue

Open client requests

Active engagement requests
RequestOwnerStatus
Liquidity reviewA. WardIn progress
Board pack refreshM. KhanReady
<Panel title="Engagement queue" description="Open client requests">
  <Tabs defaultValue="active">
    <TabsList>
      <TabsTab value="active">Active</TabsTab>
      <TabsTab value="blocked">Blocked</TabsTab>
    </TabsList>
    <TabsPanel value="active">
      <DataTable
        caption="Active engagement requests"
        dense
        rows={[
          { request: "Liquidity review", owner: "A. Ward", status: "In progress", tone: "primary" as const },
          { request: "Board pack refresh", owner: "M. Khan", status: "Ready", tone: "success" as const },
        ]}
        columns={[
          { key: "request", header: "Request", cell: (row) => row.request, cellClassName: "font-medium" },
          { key: "owner", header: "Owner", cell: (row) => row.owner },
          { key: "status", header: "Status", cell: (row) => <Badge variant={row.tone}>{row.status}</Badge> },
        ]}
      />
    </TabsPanel>
    <TabsPanel value="blocked">
      <EmptyState
        title="No blocked requests"
        description="Requests with missing inputs will appear here."
      />
    </TabsPanel>
  </Tabs>
</Panel>

When to use it

Use it for

  • Sibling views of the same object, such as engagement summary, people, finance, and risk.
  • Compact dashboard panels where switching content avoids a second page or route.
  • Controlled tab state when adjacent status, filters, or analytics need the selected value.

Reach for something else

  • Primary application navigation across routes — use DashboardShell navigation instead.
  • A short list of commands or links — use Menu instead.
  • One finite form choice inside a field — use Select instead.

Props

PropTypeDefaultDescription
TabsdefaultValue?, value?, onValueChange?The controller (Base UI's Tabs.Root). Set a default, or control it.
TabsListchildrenThe horizontal bar — renders the bottom border and the moving underline indicator.
TabsTabvalue: stringA single tab trigger; its value pairs it with the TabsPanel of the same value.
TabsPanelvalue: stringThe content shown for the matching tab. Only the active panel is visible.

See also