# Tabs

> A keyboard-accessible tab panel with `<Tab eventKey>` children and an optional controlled `activeKey`. Arrow keys move focus; Enter activates.

- Category: navigation
- Status: stable (since 0.1.0)
- A11y pattern: https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
- Tokens: --foreground-primary, --foreground-secondary, --foreground-quaternary, --background-tertiary
- Playground: https://design.freecodecamp.org/playground#tabs
- npm dependencies: `react@>=18 <20`, `@ark-ui/react@^5.0.0`
- Registry dependencies: [theme](https://design.freecodecamp.org/registry/theme.md)
- Files:
  - `Tabs.tsx` → `src/ui/tabs/Tabs.tsx` (raw: https://design.freecodecamp.org/registry/tabs/Tabs.tsx)
  - `tabs.css` → `src/ui/tabs/tabs.css` (raw: https://design.freecodecamp.org/registry/tabs/tabs.css)

## Install (copy source)

1. Ensure the theme is installed once per project - tokens.css + base.css imported globally, fonts available. See https://design.freecodecamp.org/registry/theme.md and https://design.freecodecamp.org/registry/starter.md.
2. Copy the files below into `src/ui/tabs/` (adjust to your project layout) and import the CSS once from your global stylesheet, e.g. `@import './ui/tabs/tabs.css';`.
3. Colors, spacing and type come from tokens - tailor the component by editing the copied source; recolour by editing tokens.css, not the component CSS.

## Usage

`<Tabs>` + `<Tab>` render a proper `role="tablist"` / `role="tab"` /
`role="tabpanel"` triad. Internally composed on `@ark-ui/react/tabs`,
so Arrow keys move focus between triggers, Home/End jump to the ends,
and Enter / Space activate. Works controlled via `activeKey` or
uncontrolled via `defaultActiveKey`.

## Keyboard

| Key               | Action                            |
| ----------------- | --------------------------------- |
| ArrowLeft / Right | Cycle triggers horizontally.      |
| ArrowUp / Down    | Cycle triggers when vertical.     |
| Home / End        | Jump to the first / last trigger. |
| Enter / Space     | Activate the focused trigger.     |
| Tab               | Moves focus out of the tablist.   |

## Accessibility

Full APG [tabs](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/)
compliance. Triggers expose `aria-selected` and `data-selected=""`;
panels expose `role="tabpanel"` + `data-state="open|closed"`. Inactive
panels receive the `hidden` attribute so screen readers skip them.

## Example

```tsx
import { Tabs, Tab } from './ui/tabs/Tabs';

<Tabs defaultActiveKey="instructions">
  <Tab eventKey="instructions" title="Instructions">...</Tab>
  <Tab eventKey="tests" title="Tests">...</Tab>
  <Tab eventKey="console" title="Console">...</Tab>
</Tabs>
```

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `activeKey` | `string` | no | - |  |
| `defaultActiveKey` | `string` | no | - |  |
| `onSelect` | `((key: string) => void)` | no | - |  |

## Source: Tabs.tsx

```tsx
import React, { Children, isValidElement } from 'react';
import { Tabs as Ark } from '@ark-ui/react/tabs';

export interface TabProps extends Omit<
  React.HTMLAttributes<HTMLDivElement>,
  'title'
> {
  eventKey: string;
  title: React.ReactNode;
  children?: React.ReactNode;
}

export function Tab(_props: TabProps): null {
  // Rendered indirectly by <Tabs>. Config shell only.
  return null;
}
Tab.displayName = 'Tab';

export interface TabsProps extends Omit<
  React.HTMLAttributes<HTMLDivElement>,
  'onChange' | 'onSelect' | 'defaultValue'
> {
  activeKey?: string;
  defaultActiveKey?: string;
  onSelect?: (key: string) => void;
  children?: React.ReactNode;
}

export function Tabs({
  activeKey,
  defaultActiveKey,
  onSelect,
  className = '',
  children,
  ...rest
}: TabsProps): React.ReactElement {
  const tabs = Children.toArray(children).filter(
    (c): c is React.ReactElement<TabProps> =>
      isValidElement(c) && (c.type as React.ComponentType) === Tab
  );
  const fallback = tabs[0]?.props.eventKey ?? '';
  const rootClass = ['tabs', className].filter(Boolean).join(' ');

  return (
    <Ark.Root
      className={rootClass}
      value={activeKey}
      defaultValue={
        activeKey === undefined ? (defaultActiveKey ?? fallback) : undefined
      }
      onValueChange={details => onSelect?.(details.value)}
      {...rest}
    >
      <Ark.List className='tabs__list'>
        {tabs.map(t => (
          <Ark.Trigger
            key={t.props.eventKey}
            value={t.props.eventKey}
            className='tabs__tab'
          >
            {t.props.title}
          </Ark.Trigger>
        ))}
      </Ark.List>
      {tabs.map(t => (
        <Ark.Content
          key={t.props.eventKey}
          value={t.props.eventKey}
          className='tabs__panel'
        >
          {t.props.children}
        </Ark.Content>
      ))}
    </Ark.Root>
  );
}
Tabs.displayName = 'Tabs';
```

## Source: tabs.css

```css
.tabs {
}
.tabs__list {
  display: flex;
  border-bottom: 1px solid var(--foreground-quaternary);
  padding: 0;
  margin: 0;
  list-style: none;
}
.tabs__tab {
  flex: 1;
  padding: 8px 14px;
  background: transparent;
  border: 0;
  cursor: pointer;
  font: 400 var(--fs-md)/1 var(--font-sans);
  color: var(--foreground-secondary);
}
.tabs__tab:hover {
  background: var(--background-tertiary);
}
.tabs__tab[aria-selected='true'] {
  font-weight: 700;
  background: var(--foreground-quaternary);
  color: var(--background-secondary);
}
.tabs__panel {
  padding: 18px 2px;
}
```

## HTML / vanilla variant

```html
<div class="tabs">
  <div role="tablist" class="tabs__list">
    <button class="tabs__tab" aria-selected="true">Instructions</button>
    <button class="tabs__tab" aria-selected="false">Tests</button>
    <button class="tabs__tab" aria-selected="false">Console</button>
  </div>
  <div class="tabs__panel">...</div>
</div>
```

Interactive behaviours for plain HTML come from the vanilla runtime (data-uikit-* attributes): https://design.freecodecamp.org/registry/vanilla.md - or download https://design.freecodecamp.org/cdn/uikit.global.js once and self-host it (do not hotlink).

## For coding agents

This library is distributed as copyable source, not an npm package. Start at https://design.freecodecamp.org/registry/starter.md, discover components via https://design.freecodecamp.org/llms.txt, and copy files into the consuming project. Keep token names intact; recolour by editing the copied tokens.css.
