# Tabs

One strip, several panels.

- Group: Navigation
- Import: `import { Tabs } from '@misoto22/folio'`
- Page: https://ui.misoto22.com/components/tabs/
- Related: accordion

## Anatomy

- **Root** (required) — Tabs — Radix’s root, re-exported. It draws nothing and owns everything: value or defaultValue, and activationMode. With neither value nor defaultValue no tab is selected and no panel is mounted.
- **Tab strip** (required) — TabsList — the role="tablist" row, sitting on one hairline rule and scrolling on its own axis with scroll-slim. It has no accessible name unless you give it one.
- **Tab** (required) — TabsTrigger, at the md control height with its label on one line. Every tab carries the 2px active marker, transparent until it is the selected one; the marker is pulled onto the strip’s own border with -mb-px so the two share a line rather than stacking into a 3px edge.
- **Panel** — TabsContent, paired to its tab by matching value. Mounted only while it is the selected one, and marked data-folio-animated so its entrance is dropped for a reader who asked for less motion.

## Best practices

### Do

- Give the root a defaultValue or a value: with neither, nothing matches, every panel stays unmounted, and the page renders a strip above an empty space with nothing to say what is missing.
- Match each trigger’s value to a panel’s value exactly — the pairing is string equality, and a typo is not an error but a tab that opens onto nothing.
- Pass activationMode="manual" when a panel fetches or renders something expensive: the default is automatic, so ← and → select as they move and arrowing across four tabs starts four loads before the reader has stopped.
- Give TabsList an aria-label on a page with more than one set: Radix names the tablist after nothing, and two unnamed tablists are two “tab list”s a reader cannot tell apart.

### Don’t

- An unselected panel is unmounted, not hidden — find-in-page cannot reach its text, a print takes only the panel that was open, and a half-filled form in another tab has lost what was typed into it by the time the reader comes back.
- The selected tab lives in React state, not in the URL: a reader who reloads or shares the page lands on the first panel, so anything worth linking to needs value lifted into a query parameter.
- Tabs are not a way to fit more in: the strip scrolls silently, and a sixth tab past the fold looks exactly like a page that only has five.

## Accessibility

- The strip scrolls rather than wrapping: a wrapped second row moves every tab below it and the reader loses the one they were about to click.
- 44px tall, because a tab is a pointer target like any other.

## Keyboard

- Tab — Moves into the strip, and out of it — the whole strip is one stop.
- ← / → — Moves between tabs and switches the panel with them.
- Home / End — Jumps to the first or last tab.

## Tabs

Accessible tabs with a moving marker and optional horizontal drag gestures.

## TabsList

The tab strip. Scrolls horizontally rather than wrapping. A wrapped second row of tabs moves every tab below it when the strip grows, and the reader loses the one they were about to click.

## TabsTrigger

One tab. The active marker is a 2px ink rule pulled onto the strip's own border with `-mb-px`, so the two occupy the same line instead of stacking into a 3px edge. 44px tall, because a tab is a pointer target like any other.

## TabsContent

The panel paired to a TabsTrigger by matching `value`.

## Example — default

```tsx
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@misoto22/folio'

<Tabs defaultValue="preview" className="w-full">
  <TabsList>
    <TabsTrigger value="preview">Preview</TabsTrigger>
    <TabsTrigger value="code">Code</TabsTrigger>
    <TabsTrigger value="props">Props</TabsTrigger>
  </TabsList>
  <TabsContent value="preview" className="text-sm text-(--ink-2)">The rendered component.</TabsContent>
  <TabsContent value="code" className="text-sm text-(--ink-2)">The source that produced it.</TabsContent>
  <TabsContent value="props" className="text-sm text-(--ink-2)">Its parsed prop table.</TabsContent>
</Tabs>
```

## Example — manual activation

```tsx
import { Tabs, TabsContent, TabsList, TabsTrigger, Text } from '@misoto22/folio'

<Tabs defaultValue="traffic" activationMode="manual" className="w-full">
  <TabsList aria-label="Site metrics">
    <TabsTrigger value="traffic">Traffic</TabsTrigger>
    <TabsTrigger value="referrers">Referrers</TabsTrigger>
    <TabsTrigger value="errors">Errors</TabsTrigger>
  </TabsList>
  <TabsContent value="traffic">
    <Text size="sm">18,402 visits over the last 30 days, up 6% on the month before.</Text>
  </TabsContent>
  <TabsContent value="referrers">
    <Text size="sm">news.ycombinator.com sent 4,110 of them; nothing else broke 500.</Text>
  </TabsContent>
  <TabsContent value="errors">
    <Text size="sm">Eleven 500s, all from the same deploy, all before 09:14.</Text>
  </TabsContent>
</Tabs>
```

## Example — what gets hidden

```tsx
<div className="grid w-full gap-8 lg:grid-cols-3">
  <div className="flex flex-col gap-3">
    <Text size="xs" tone="muted">
      Tabs — one of several views
    </Text>
    <Tabs defaultValue="usd">
      <TabsList aria-label="Currency">
        <TabsTrigger value="usd">USD</TabsTrigger>
        <TabsTrigger value="aud">AUD</TabsTrigger>
      </TabsList>
      <TabsContent value="usd">
        <Text size="sm">Billed at $18 a seat each month.</Text>
      </TabsContent>
      <TabsContent value="aud">
        <Text size="sm">Billed at A$28 a seat each month.</Text>
      </TabsContent>
    </Tabs>
  </div>
  <div className="flex flex-col gap-3">
    <Text size="xs" tone="muted">
      Accordion — a set of answers
    </Text>
    <Accordion type="single" collapsible>
      <AccordionItem value="usd" title="What does a seat cost?">
        Billed at $18 a seat each month.
      </AccordionItem>
      <AccordionItem value="cancel" title="Can I cancel mid-month?">
        Yes, and the remainder is refunded.
      </AccordionItem>
    </Accordion>
  </div>
  <div className="flex flex-col gap-3">
    <Text size="xs" tone="muted">
      Collapsible — detail on one thing
    </Text>
    <div className="border-y border-(--rule)">
      <CollapsibleSection title="Billing detail">
        Billed at $18 a seat each month, on the day you first subscribed.
      </CollapsibleSection>
    </div>
  </div>
</div>
```
