Skip to content

shadcn.io is an independent project built on shadcn/ui

Free

Nested Tabs for React

Profile, Settings and Billing, and each panel has its own row of tabs. Profile opens on Personal Info for John Doe.

Profile, Settings and Billing sit on the outer row. Profile is selected, and inside it Personal Info, Social Links and Preferences switch a short list of labeled values. Personal Info shows John Doe.

Use a second row when every section has subsections, such as an account page. A Navigation Menu fits if those sections are destinations, not panels. In React, Base UI does not write the rows of labeled values.

Preview

Preview width

Installation

Sign in to get your install command
Free with an account. It carries your token, so the CLI installs everything in one go.
Use email insteadNo credit card

Anatomy

Tabs anatomy: root, list, indicator, trigger, content

Usage

<Tabs className="w-full max-w-2xl" defaultValue="profile">
  <TabsList>
    {sections.map((section) => (
      <TabsTrigger key={section.value} value={section.value}>
        {section.label}
      </TabsTrigger>
    ))}
  </TabsList>
  {sections.map((section) => (
    <TabsContent key={section.value} value={section.value}>
      <div className="rounded-lg border bg-card p-6 text-card-foreground shadow-sm">
        <h3 className="mb-4 font-semibold text-lg">{section.label}</h3>
        <Tabs defaultValue={section.defaultValue}>
          <TabsList>
            {section.tabs.map((tab) => (
              <TabsTrigger key={tab.value} value={tab.value}>
                {tab.label}
              </TabsTrigger>
            ))}
          </TabsList>
          {section.tabs.map((tab) => (
            <TabsContent key={tab.value} value={tab.value}>
              <div className="mt-4 flex flex-col gap-3 rounded-md border p-4">
                {tab.rows.map((row) => (
                  <div key={row.label}>
                    <p className="font-medium text-sm">{row.label}</p>
                    <p className="text-muted-foreground text-sm">
                      {row.value}
                    </p>
                  </div>
                ))}
{/* The rest of the file is under Installation. */}

FAQ

How is the outer tab set up?

Tabs starts on profile and is w-full max-w-2xl. The triggers are Profile, Settings and Billing, built from a sections array, and each panel is a bordered block headed by the section name.

Where do the second tabs come from?

Each outer panel renders its own Tabs from the section's tabs. Profile starts on Personal Info, Settings on General and Billing on Current Plan.

Does the inner tab stay put when I leave the outer tab?

No. Inactive panels unmount, so the inner tabs mount again at their default when you come back. Set keepMounted on each TabsContent to keep the inner tabs mounted.

What is inside an inner panel?

A bordered box of rows, each a font-medium label over muted text. Personal Info shows Full Name, Email and Location.

Do the inner lists look different from the outer list?

No. Both use TabsList and TabsTrigger with no extra classes. The inner list sits under the panel heading.

Do the inner values collide with the outer ones?

No. Each Tabs matches only the triggers and panels inside it. profile belongs to the outer tabs, and personal, general and plan to the inner ones.

API Reference

Tabs

Sets defaultValue="profile", value={section.value}, defaultValue={section.defaultValue} and value={tab.value}. The rest is on the Tabs page.