---
id: "packages/active-ui/responsive-tabs"
title: "ResponsiveTabs"
description: "Tabs that become a menu on narrow screens, and a variant that keeps the current tab in the address."
url: "/packages/active-ui/responsive-tabs"
locale: "en"
revision: "5f92f2afbd1d74cc"
---

# ResponsiveTabs

`ResponsiveTabs` shows [tabs](/packages/active-ui/tabs) from the `lg` width (1024 px) and a
full-width menu button below it, so many tabs never overflow a phone. `SearchParamTabs` is the same
component with the current tab in a search parameter of the address, so a link or a reload opens
the same tab.

**Example: Tabs that become a menu**

Client: Runs entirely in your browser. Source: `examples/ui/responsive-tabs.tsx`.

```tsx
// apps/docs/examples/ui/responsive-tabs.tsx
'use client';

import {useState} from 'react';
import {FileTextIcon, HistoryIcon, UserIcon} from 'lucide-react';

import {
  ResponsiveTabs,
  TabPanel,
} from '@workspace/active-ui/components/composed/responsive-tabs';

const TABS = [
  {value: 'person', label: 'Person', icon: UserIcon},
  {value: 'documents', label: 'Documents', icon: FileTextIcon},
  {value: 'history', label: 'History', icon: HistoryIcon},
];

/**
 * Tabs on a wide screen, a menu on a narrow one: make the window narrower
 * than 1024 px to see the menu. The panels are the same in both forms.
 */
export default function ResponsiveTabsExample() {
  const [tab, setTab] = useState('person');
  return (
    <div className="w-full max-w-xl">
      <ResponsiveTabs
        value={tab}
        onValueChange={setTab}
        tabs={TABS}
        mobilePlaceholder="Choose a section"
        desktopContentClassName="pt-4"
      >
        <TabPanel id="person">Name, date of birth and address.</TabPanel>
        <TabPanel id="documents">Statements and letters.</TabPanel>
        <TabPanel id="history">Every change, newest first.</TabPanel>
      </ResponsiveTabs>
    </div>
  );
}
```

## Keeping the tab in the address

**Example: Tabs kept in the address**

Client: Runs entirely in your browser. Source: `examples/ui/search-param-tabs.tsx`.

```tsx
// apps/docs/examples/ui/search-param-tabs.tsx
'use client';

import {TabPanel} from '@workspace/active-ui/components/composed/responsive-tabs';
import {SearchParamTabs} from '@workspace/active-ui/components/composed/search-param-tabs';

/**
 * The same tabs, with the current tab in the address: choosing a tab sets
 * `?example-tab=` on this page, so a link or a reload opens it again. The
 * default tab leaves the parameter out, and an unknown value falls back to it.
 */
export default function SearchParamTabsExample() {
  return (
    <div className="w-full max-w-xl">
      <SearchParamTabs
        queryParamName="example-tab"
        defaultValue="summary"
        tabs={[
          {value: 'summary', label: 'Summary'},
          {value: 'payments', label: 'Payments'},
        ]}
        mobilePlaceholder="Choose a section"
        desktopContentClassName="pt-4"
      >
        <TabPanel id="summary">The totals of this year.</TabPanel>
        <TabPanel id="payments">Every payment of this year.</TabPanel>
      </SearchParamTabs>
    </div>
  );
}
```

`SearchParamTabs` reads and writes one search parameter (`queryParamName`, default `tab`) with
the Next.js App Router. Choosing the default tab removes the parameter; a value that matches no tab
shows the default tab and leaves the address as it is. Each choice adds a history entry, without
scrolling.

## What the composition adds

- **Two forms, one set of panels.** On a wide screen a tab list; on a narrow one a button with the
  current tab's label that opens a menu with one radio item per tab. `TabPanel` renders the
  panels in both forms.
- **Icons** per tab, before the label, in both forms.

## Usage

```tsx
import {
  ResponsiveTabs,
  TabPanel,
} from '@workspace/active-ui/components/composed/responsive-tabs';

<ResponsiveTabs
  value={tab}
  onValueChange={setTab}
  tabs={[
    {value: 'person', label: 'Person'},
    {value: 'history', label: 'History'},
  ]}
  mobilePlaceholder="Choose a section"
>
  <TabPanel id="person">…</TabPanel>
  <TabPanel id="history">…</TabPanel>
</ResponsiveTabs>;
```

## What your application provides

- The tabs and one `TabPanel` per tab, with the tab's `value` as its `id`.
- For `SearchParamTabs`: a Next.js App Router page, and a parameter name that does not clash with
  others on the page.

## Accessibility and keyboard

- Wide: a tab list as on [Tabs](/packages/active-ui/tabs); the arrow keys move between tabs, and
  `Enter` or `Space` activates one.
- Narrow: the button opens a menu; the current tab is the checked radio item, the arrow keys move,
  `Enter` chooses and closes the menu.

## API

### ResponsiveTabs

**ResponsiveTabs**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | yes | One `TabPanel` per tab. |
| `mobilePlaceholder` | `string` | yes | Text on the menu button when no tab matches `value`. Supplied translated. |
| `onValueChange` | `(value: string) => void` | yes | Receives the value of the chosen tab. |
| `tabs` | `readonly TabConfig[]` | yes | The tabs, in order. |
| `value` | `string` | yes | The value of the current tab. |
| `desktopClassName` | `string` | no | Classes for the wrapper of the tab list on wide screens. |
| `desktopContentClassName` | `string` | no | Classes for the wrapper of the panels. |

**TabPanel**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | yes | The panel content. |
| `id` | `string` | yes | The `value` of the tab this panel belongs to. |
| `className` | `string` | no | Classes for the panel. |

**ResponsiveTabsProps**

```ts
interface ResponsiveTabsProps {
  value: string;
  onValueChange: (value: string) => void;
  tabs: readonly TabConfig[];
  children: React.ReactNode;
  desktopClassName?: string;
  desktopContentClassName?: string;
  mobilePlaceholder: string;
}
```

**TabConfig**

```ts
interface TabConfig {
  value: string;
  label: React.ReactNode;
  icon?: React.ComponentType<{className?: string}>;
}
```

One tab: its identity, its label and an optional icon.

**TabPanelProps**

```ts
interface TabPanelProps {
  id: string;
  children: React.ReactNode;
  className?: string;
}
```

### SearchParamTabs

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | yes | One `TabPanel` per tab. |
| `defaultValue` | `string` | yes | The default tab value to use when the query parameter is not present in the URL. |
| `mobilePlaceholder` | `string` | yes | Text on the menu button when no tab matches `value`. Supplied translated. |
| `tabs` | `readonly TabConfig[]` | yes | The tabs, in order. |
| `desktopClassName` | `string` | no | Classes for the wrapper of the tab list on wide screens. |
| `desktopContentClassName` | `string` | no | Classes for the wrapper of the panels. |
| `queryParamName` | `string` | no | The name of the URL search parameter to sync with the active tab state. |
