# Tabs

**Maturity:** stable

A control that presents several mutually-exclusive views and lets the user switch between them by tapping a labelled tab — the canonical glass tab bar.

## Import

```tsx
import { Tabs } from "@liquidify/react/tabs"
```

## Props

- `aria-label`: `string | undefined`. Accessible name for the bar — the `role="tablist"` in tab mode, the `<nav>` landmark in navigation mode. Required (no default text).
- `aria-labelledby`: `string | undefined`. Accessible name reference for the bar (tablist or `<nav>` landmark).
- `children`: `ReactNode` (required). The tabs, rendered in order — 1..N {@link Tabs.Item} children (children role: `items`). Only `Tabs.Item` children are read; any non-`Item` node (a `Fragment`, a wrapper, stray text) is a **dev-mode `console.error`** and is filtered in production.
- `className`: `string | undefined`. Optional additional CSS class merged into the glass surface class. The supported per-instance customisation seam: override any `--lq-*` custom property from this class's CSS — the token cascade the engine reads (ADR-0004). No raw inline style is involved, so this stays ADR-0003 clean.
- `contentPlacement`: `"top" | "bottom" | undefined`; default ``"top"``. Which side of the tab bar the active tab's content (the `Tabs.Item` children) is rendered on. `"top"` — the reference iOS arrangement: the panel sits **above** a bottom tab bar. `"bottom"` — the bar leads and the panel sits **below** it, the familiar web/macOS arrangement. The two regions are reordered in the DOM, not merely visually, so the reading and focus order always match what is on screen (APG Tabs). The root also stamps `data-content-placement` for styling. Invalid in navigation mode (no panel exists) — a dev-mode `console.error`, and the prop is ignored.
- `defaultValue`: `string | undefined`; default `the first `Tabs.Item`'s `value` (tab mode only)`. Uncontrolled seed for the internal selection (ignored once {@link TabsProps.value} is supplied). Invalid in navigation mode — a nav bar's selection is route-derived, so an uncontrolled seed is a dev-mode `console.error` and is ignored (an unmatched route must select nothing, never fall back to the first link).
- `disabled`: `boolean | undefined`. When `true`, the whole control is dimmed, removed from the tab order, and all interaction is suppressed.
- `onChange`: `((value: string) => void) | undefined`. Fires with the newly selected tab's `value` on activation. A same-value activation is dropped (no-op policy, ADR-0015). In navigation mode this is the SPA interception hook: when wired, a plain left-click is `preventDefault()`ed and `onChange` fires with the item's identity (`value ?? href`) so the consumer's router owns the transition; modified clicks always navigate natively.
- `orientation`: `"horizontal" | "vertical" | undefined`; default ``"horizontal"``. Layout axis of the tab bar. Only `"horizontal"` is implemented in v1 (spec `## Out of scope`); the root still stamps `data-orientation`.
- `ref`: `Ref<HTMLDivElement> | undefined`. Forwarded ref to the rendered element — the component's native root (or the merged child element on a component that opts into {@link SlottableProps}). Generic over `E` (charter `docs/api-conventions.md` §4 rule 1, default `HTMLElement`) so a component with a polymorphic root (Link's navigation / action mode union, Text's `as`) can instantiate the concrete element per variant instead of leaving every consumer with a widened `Ref<HTMLElement>`. A component that does not narrow simply inherits the `HTMLElement` default — source-compatible with every pre-existing non-generic `extends ControlProps`.
- `tint`: `LiquidTintValue | undefined`. System-accent of the control — drawn from the RESTRICTED {@link LIQUID_COLORS} palette (the thirteen Apple system colours). **Orthogonal to a component's `variant`**: it recolours the accent the surface draws from (focus ring, tinted fill, selection) without changing its variant. Resolved via `data-tint` attribute selectors in CSS (tokens-only, ADR-0003).
- `value`: `string | undefined`. Controlled selected-tab key. Passing this switches Tabs to controlled mode: the rendered selection always reflects this value and the consumer owns it via {@link TabsProps.onChange}.

## SwiftUI mapping

- ``TabView { … }`` → ``<Tabs>…</Tabs>`` (direct): Container. Compound children model mirroring `SegmentedControl` (Q2, amended 2026-08-02); there is no `items` data-array prop.
- ``TabView(selection: $sel)`` → ``value?: string` (controlled) + `onChange(next: string)`` (direct): Not shown in the demo; the React convention adds the controlled triad (ADR-0015). `Binding` splits into `value` + `onChange`. Controlled iff `value !== undefined` (audit §8).
- `(implicit first-tab default)` → ``defaultValue?: string` (default the first `Tabs.Item`'s `value`)` (direct): Uncontrolled seed; mirrors SegmentedControl exactly.
- ``.tabItem { Label(title, systemImage: icon) }` applied to a content view` → ``<Tabs.Item value label icon badge? disabled?>{panel}</Tabs.Item>` — `TabsItemProps = { value: string; label: ReactNode; icon: ReactNode; badge?: number | string; disabled?: boolean; children?: ReactNode }`` (direct): `title` → `label` (routed through `Text`); `systemImage:` → `icon` (stacked above the label), **required** — the reference `Label` always carries a `systemImage:`, and a caption-only bar is a SegmentedControl, not a tab bar; the tab's associated view → the `.Item`'s **`children`**, mirroring `.tabItem` modifying its content view. `value` is the stable identity.
- `(no SwiftUI counterpart — web navigation)` → ``Tabs.Item` `href?: string` (the navigation-arm discriminant) + optional `value` identity override (defaults to `href`)` (direct): React-only. An `href` item renders `<a href>` in a `<nav>` landmark; its `children` are excluded (no panel). SEO-crawlable, native middle-click/⌘-click/copy-link. See Variants "Navigation mode" and Interaction.
- ``.badge(8)` (Int) / `.badge("!")` (String)` → ``Tabs.Item` `badge?: number | string`` (direct): A single `number | string` union mirrors SwiftUI's `.badge` `Int`/`String` overload (Q3). Renders a red badge; the value is announced (Accessibility).
- ``.tint(_:)` (not shown)` → ``tint?: LiquidColor` (from `TintableControlProps`, ADR-0024)` (direct): Selected chip/label accent; default `accent` (system blue). The reference confirms the selected tint is **always** system accent regardless of the content colour behind.
- ``.disabled(true)`` → ``disabled?: boolean` (whole control) + `Tabs.Item` `disabled` (per tab)` (direct): Whole-control: `data-disabled` + `aria-disabled` + `pointer-events: none` + removed from tab order. Per-item skips that tab in pointer + keyboard nav.
- `(implicit — the iOS bar is always bottom-anchored)` → ``contentPlacement?: "top" | "bottom"` (default `"top"`)` (direct): React-only layout axis with no SwiftUI initialiser: SwiftUI positions the `TabView` bar per platform (bottom on iOS, top on macOS/tvOS) with no consumer knob, so the web analogue exposes the choice explicitly. `"top"` = content above a bottom bar (the reference); `"bottom"` = bar above the content. Reorders the two regions in the DOM; stamps `data-content-placement` on the root.
- ``.tabViewStyle(.page)` (TabDemo)` → `—` (direct): Out of scope v1; bottom bar only.
- ``.tabViewStyle(.sidebarAdaptable)` + `TabSection` (TabDemo)` → `—` (direct): Out of scope v1; horizontal bar only.
- ``Tab(role: .search)` (TabDemo)` → `—` (direct): Out of scope v1; see Out of scope.
- ``.frame` / `.clipShape` (demo host card)` → `n/a (component-owned geometry)` (direct): The 320pt/18pt card is the **demo host** wrapping the TabView, **not** part of the bar; the bar is the floating capsule inside.
