Tabs

Tabs switch between panels of related content in the same place. Only the selected panel is rendered, and the tab bar has room for actions on the right.

import { Tabs } from 'jarvis-react-ui';

Basic

Pass the tabs as items, each with a unique value, a label and its panel content. Without value the component manages the selection itself and starts with defaultValue, or the first enabled tab.

import { CodeBlock, Tabs } from 'jarvis-react-ui';

export default function Basic() {
    return (
        <Tabs
            aria-label="Package manager"
            items={[
                {
                    value: 'npm',
                    label: 'npm',
                    content: <CodeBlock code="npm install jarvis-react-ui" />,
                },
                {
                    value: 'pnpm',
                    label: 'pnpm',
                    content: <CodeBlock code="pnpm add jarvis-react-ui" />,
                },
                {
                    value: 'yarn',
                    label: 'yarn',
                    content: <CodeBlock code="yarn add jarvis-react-ui" />,
                },
            ]}
        />
    );
}

Controlled

Pass value and onValueChange to own the selection, e.g. to sync it with the URL or to change it from elsewhere. onValueChange is also called in uncontrolled mode.

import { useState } from 'react';
import { Button, Tabs } from 'jarvis-react-ui';

export default function Controlled() {
    const [tab, setTab] = useState('status');

    return (
        <div className="flex w-full flex-col gap-3">
            <Tabs
                aria-label="Reactor"
                value={tab}
                onValueChange={setTab}
                panelClassName="text-[11px] text-text-secondary"
                items={[
                    { value: 'status', label: 'Status', content: 'Output stable at 72 percent.' },
                    { value: 'history', label: 'History', content: 'No incidents in 30 days.' },
                    { value: 'config', label: 'Config', content: 'Auto-throttle enabled.' },
                ]}
            />
            <div className="flex items-center gap-2">
                <Button size="sm" onClick={() => setTab('status')}>
                    Reset
                </Button>
                <span className="font-mono text-[10px] text-text-muted">value: {tab}</span>
            </div>
        </div>
    );
}

Disabled tab

A tab with disabled: true is dimmed, cannot be clicked and is skipped by the arrow keys.

import { Tabs } from 'jarvis-react-ui';

export default function DisabledTab() {
    return (
        <Tabs
            aria-label="Sensors"
            panelClassName="text-[11px] text-text-secondary"
            items={[
                { value: 'radar', label: 'Radar', content: '3 contacts in range.' },
                { value: 'lidar', label: 'Lidar', content: 'Calibrating.', disabled: true },
                { value: 'thermal', label: 'Thermal', content: 'No heat signatures.' },
            ]}
        />
    );
}

Actions

actions renders content at the right end of the tab bar, such as a small IconButton. Actions belong to the whole tab set, not to a single tab.

import { useState } from 'react';
import { RefreshCw } from 'lucide-react';
import { IconButton, Tabs } from 'jarvis-react-ui';

export default function WithActions() {
    const [refreshed, setRefreshed] = useState(0);

    return (
        <Tabs
            aria-label="Logs"
            panelClassName="text-[11px] text-text-secondary"
            actions={
                <IconButton
                    icon={RefreshCw}
                    label="Refresh logs"
                    size="sm"
                    onClick={() => setRefreshed((n) => n + 1)}
                />
            }
            items={[
                { value: 'all', label: 'All', content: `128 entries, refreshed ${refreshed}x.` },
                {
                    value: 'errors',
                    label: 'Errors',
                    content: `2 entries, refreshed ${refreshed}x.`,
                },
            ]}
        />
    );
}

Use panelClassName to style the panel container, e.g. its typography or padding.

Keyboard

Key Action
Tab Moves focus into the tab list (to the selected tab) and on to the panel content.
ArrowRight / ArrowLeft Selects the next / previous enabled tab, wrapping around.
Home / End Selects the first / last enabled tab.

Selection follows focus: moving with the arrow keys selects the tab immediately.

Sound

Hovering a tab plays hover_button and selecting a different tab plays click through the nearest SfxProvider. Without a provider the tabs stay silent. See Sound effects.

Accessibility

  • Follows the WAI-ARIA tabs pattern: a role="tablist" of role="tab" buttons and one role="tabpanel".
  • Name the tab list with aria-label.
  • Each tab has aria-selected and aria-controls pointing at the panel; the panel is labelled by its tab through aria-labelledby.
  • Roving tabindex: only the selected tab is in the tab order (tabIndex={0}), the others have tabIndex={-1} and are reached with the arrow keys.
  • Disabled tabs use the native disabled attribute.

API

Prop Type Default Description
items (required) TabItem[] Tabs in display order.
actions ReactNode Content rendered at the right end of the tab bar, e.g. an IconButton.
aria-label string Accessible name of the tab list.
className string Additional class names for the root element (wraps the tab bar and the panel).
defaultValue string the first enabled tab Initially selected value when uncontrolled.
onValueChange ((value: string) => void) Called with the new value when the user selects a tab.
panelClassName string Additional class names for the role="tabpanel" container.
value string Controlled selected value. Leave undefined to use defaultValue.

Type TabItem

A single tab of a Tabs component.

Field Type Default Description
content (required) ReactNode Panel content shown while the tab is selected.
label (required) ReactNode Text of the tab button.
value (required) string Unique value identifying the tab.
disabled boolean false Disables the tab.