# Next.js Bootstrap Tabs

Next.js Bootstrap tabs for the App Router: client components a server component page renders directly, with keyboard support and vertical or colored styles.

> This component is only available in **AdminKit PRO**. [Learn more](/pricing/#nextjs).

When a page has more to show than fits comfortably, tabs let the user switch between panes instead of scrolling. AdminKit gives you Bootstrap's tabs as three components: `TabContainer`, `TabLink` and `TabPane`. They switch panes with React state instead of Bootstrap's JavaScript. Tabs are part of the PRO version of [AdminKit for Next.js](/nextjs/).

In the App Router, pages are server components: they render on the server and have no state. The tab components are client components, files that start with `"use client"`, so they handle the state themselves. That means a server component page can render them directly, without a wrapper of your own.

You write the nav and the panes in Bootstrap's markup. A `TabLink` and a `TabPane` with the same `eventKey` belong together. `TabContainer` keeps the active key and adds no element to the page. All three are in `src/components/bootstrap/tabs.tsx`.

## Example

Every prop in this example is a string, so it works straight in a server component page. The demo's tabs page does the same:

```html
<div class="tab">
  <ul class="nav nav-tabs" role="tablist">
    <li class="nav-item" role="presentation">
      <a class="nav-link active" id="tabsOverviewTab" data-bs-toggle="tab" href="#tabsOverview" role="tab" aria-controls="tabsOverview" aria-selected="true">Overview</a>
    </li>
    <li class="nav-item" role="presentation">
      <a class="nav-link" id="tabsActivityTab" data-bs-toggle="tab" href="#tabsActivity" role="tab" aria-controls="tabsActivity" aria-selected="false" tabindex="-1">Activity</a>
    </li>
    <li class="nav-item" role="presentation">
      <a class="nav-link" id="tabsFilesTab" data-bs-toggle="tab" href="#tabsFiles" role="tab" aria-controls="tabsFiles" aria-selected="false" tabindex="-1">Files</a>
    </li>
  </ul>
  <div class="tab-content">
    <div class="tab-pane fade show active" id="tabsOverview" role="tabpanel" aria-labelledby="tabsOverviewTab" tabindex="0">
      <p>The status, owner and due date of the project.</p>
    </div>
    <div class="tab-pane fade" id="tabsActivity" role="tabpanel" aria-labelledby="tabsActivityTab" tabindex="0">
      <p>Comments and changes from the last 30 days.</p>
    </div>
    <div class="tab-pane fade" id="tabsFiles" role="tabpanel" aria-labelledby="tabsFilesTab" tabindex="0">
      <p>Documents and images shared with the team.</p>
    </div>
  </div>
</div>
```
```tsx code title="src/app/(dashboard)/projects/page.tsx"
import { TabContainer, TabLink, TabPane } from "@/components/bootstrap/tabs";
import { pageMetadata } from "@/lib/site";

export const metadata = pageMetadata("/projects");

export default function ProjectsPage() {
  return (
    <div className="tab">
      <TabContainer defaultActiveKey="overview">
        <ul className="nav nav-tabs" role="tablist">
          <li className="nav-item" role="presentation">
            <TabLink eventKey="overview" className="nav-link">Overview</TabLink>
          </li>
          <li className="nav-item" role="presentation">
            <TabLink eventKey="activity" className="nav-link">Activity</TabLink>
          </li>
          <li className="nav-item" role="presentation">
            <TabLink eventKey="files" className="nav-link">Files</TabLink>
          </li>
        </ul>
        <div className="tab-content">
          <TabPane eventKey="overview">
            <p>The status, owner and due date of the project.</p>
          </TabPane>
          <TabPane eventKey="activity">
            <p>Comments and changes from the last 30 days.</p>
          </TabPane>
          <TabPane eventKey="files">
            <p>Documents and images shared with the team.</p>
          </TabPane>
        </div>
      </TabContainer>
    </div>
  );
}
```

The server renders every pane and shows the `defaultActiveKey` one. The tabs respond as soon as the page hydrates, that is, once the browser has taken over the HTML Next.js built.

`.tab` is AdminKit's tab style: borderless tabs above a card-like `.tab-content`. Without it you get Bootstrap's plain `.nav-tabs`. For pills, put `nav nav-pills` on the list.

## Props

`TabContainer`:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultActiveKey` | `string` | | The tab that is active first |
| `activeKey` | `string` | | Controlled: the active tab; use it with `onSelect` |
| `onSelect` | `(key: string) => void` | | Called with the `eventKey` of the tab that was selected |

`TabLink`:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `eventKey` | `string` | | The key of the pane it shows |
| `as` | `ElementType` | `"a"` | The element: an `a` with `href="#"`, or `"button"` (gets `type="button"`) |
| `disabled` | `boolean` | | Adds `.disabled`; a click does nothing and the arrow keys skip it |

`TabPane`:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `eventKey` | `string` | | The key of its tab |
| `fade` | `boolean` | `true` | Fades the pane in (`.fade`); `false` switches at once |

Other props, such as `className`, go to the `TabLink` and `TabPane` elements. The `id`, `aria-controls` and `aria-labelledby` attributes that connect a tab and its pane come from `useId`. That way the server HTML and the browser agree on them.

A server component can only pass serializable props: values, such as strings, that Next.js can send to the browser as data. Functions and components are not, so `onSelect`, `onClick` and an `as` component need a client component. A tag name such as `"button"` is a string, so `as="button"` works from a server component.

## Controlled tabs

Sometimes a button outside the nav should open a tab. Then a client component owns the active tab, through `activeKey` and `onSelect`:

```tsx title="src/components/project-tabs.tsx"
"use client";

import { useState } from "react";
import { TabContainer, TabLink, TabPane } from "@/components/bootstrap/tabs";

export function ProjectTabs() {
  const [tab, setTab] = useState("overview");

  return (
    <>
      <button type="button" className="btn btn-light mb-3" onClick={() => setTab("files")}>
        Show files
      </button>
      <TabContainer activeKey={tab} onSelect={setTab}>
        {/* the nav and the panes */}
      </TabContainer>
    </>
  );
}
```

## Colored and vertical tabs

Color and direction are classes on the wrapper, so a server component can set them. A theme color (`tab-primary`, `tab-success`, `tab-danger`, ...) colors the active tab and the content. `tab-vertical` moves the tabs to the left. The demo panes use `.tab-title` for their heading.

```html
<div class="tab tab-vertical tab-success">
  <ul class="nav nav-tabs" role="tablist">
    <li class="nav-item" role="presentation">
      <a class="nav-link active" id="tabsVerticalPlanTab" data-bs-toggle="tab" href="#tabsVerticalPlan" role="tab" aria-controls="tabsVerticalPlan" aria-selected="true">Plan</a>
    </li>
    <li class="nav-item" role="presentation">
      <a class="nav-link" id="tabsVerticalUsageTab" data-bs-toggle="tab" href="#tabsVerticalUsage" role="tab" aria-controls="tabsVerticalUsage" aria-selected="false" tabindex="-1">Usage</a>
    </li>
  </ul>
  <div class="tab-content">
    <div class="tab-pane active show" id="tabsVerticalPlan" role="tabpanel" aria-labelledby="tabsVerticalPlanTab" tabindex="0">
      <h2 class="h4 tab-title">Plan</h2>
      <p>Your current plan and its renewal date.</p>
    </div>
    <div class="tab-pane" id="tabsVerticalUsage" role="tabpanel" aria-labelledby="tabsVerticalUsageTab" tabindex="0">
      <h2 class="h4 tab-title">Usage</h2>
      <p>Seats and storage used this month.</p>
    </div>
  </div>
</div>
```
```tsx code
<div className="tab tab-vertical tab-success">
  <TabContainer defaultActiveKey="plan">
    <ul className="nav nav-tabs" role="tablist">
      <li className="nav-item" role="presentation">
        <TabLink eventKey="plan" className="nav-link">Plan</TabLink>
      </li>
      <li className="nav-item" role="presentation">
        <TabLink eventKey="usage" className="nav-link">Usage</TabLink>
      </li>
    </ul>
    <div className="tab-content">
      <TabPane eventKey="plan" fade={false}>
        <h2 className="h4 tab-title">Plan</h2>
        <p>Your current plan and its renewal date.</p>
      </TabPane>
      <TabPane eventKey="usage" fade={false}>
        <h2 className="h4 tab-title">Usage</h2>
        <p>Seats and storage used this month.</p>
      </TabPane>
    </div>
  </TabContainer>
</div>
```

Icon tabs work from a server component too. Put a Lucide icon in the `TabLink` and give the link an `aria-label`. The demo does it with `<TabLink eventKey="settings" class="nav-link" aria-label="Settings"><Settings class="align-middle" /></TabLink>`. The `.tab` style makes the icon 20px.

## Accessibility

- `TabLink` sets `role="tab"`, `aria-selected` and `aria-controls`. `TabPane` sets `role="tabpanel"`, `aria-labelledby` and `tabIndex={0}`. You add `role="tablist"` on the list and `role="presentation"` on its items.
- The active tab is the only tab in the Tab order. Arrow keys, Home and End move between the tabs of the same `role="tablist"` and show the pane, as in Bootstrap 5.3. They do nothing when the list has no `role="tablist"`.
- Give icon-only tabs an `aria-label`.
- `disabled` adds the `.disabled` class only. Add `aria-disabled="true"` for screen readers.

## Related

- [React Bootstrap tabs](/react/docs/navigation/tabs/): the same component in AdminKit for React (Vite)
- [Bootstrap 5 navs and tabs](/bootstrap/docs/navigation/navs-tabs/): the nav and tab classes of the HTML version
- [Accordion](/nextjs/docs/components/accordion/): another way to show one section at a time
- [Tabs demo](https://nextjs-demo.adminkit.io/ui-tabs)

---

Canonical HTML page: https://adminkit.io/nextjs/docs/navigation/tabs/

Site overview for agents: https://adminkit.io/llms.txt (expanded: https://adminkit.io/llms-full.txt) · Documentation: https://adminkit.io/bootstrap/docs/ · Questions: support@adminkit.io
