Skip to content

Tabs

Controlled tab set sharing state across list/triggers/content; inactive panels are not mounted.

  • Category: Navigation & disclosure
  • Status: stable public Registry/export-map component family
  • Targets: iOS · Android · Web, subject to the compatibility contract
  • Prerequisites: @beemvp/beeui-ui installed (or this component’s source copied via the Registry CLI below) and, on Web, the BeeUI Tailwind/Uniwind theme CSS loaded — see Start for full platform setup.
  • Source: packages/ui/src/components/tabs.tsx
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@beemvp/beeui-ui';

There is no documented deep/private source import. For source ownership from a BeeUI checkout:

Terminal window
pnpm beeui add tabs

Registry (used throughout this page) is BeeUI’s source-ownership manifest — registry/registry.json — that the pnpm beeui/@beemvp/beeui-cli CLI reads to copy a component’s real source into your app and rewrite its internal imports; it is not an npm package index. This component’s own Registry entry: registry/registry.json.

  • Family exports: Tabs TabsContent TabsList TabsTrigger
    • Also routed here, outside the Registry family:
      • tabs
    • Package export subpath: @beemvp/beeui-ui/tabs

Exported types: TabsContentProps, TabsListProps, TabsProps, TabsTriggerProps

The generated API inventory is mechanically joined to packages/ui/src/index.ts, Registry metadata, and the component reference contract. Each type’s field table below is parsed directly from that source, not a second hand-maintained copy; for the fuller behavior narrative see the canonical component behavior catalog.

Fully controlled (value/onValueChange, required — there is no uncontrolled mode) tab state shared across TabsList/TabsTrigger/TabsContent; enabled usage without onValueChange warns in development, and an inactive TabsContent panel is not mounted. TabsList’s scrollable renders the strip inside a horizontal scroll container instead of an equal-width flex row, scrolling the selected TabsTrigger into view whenever value changes; addon renders extra pinned content (e.g. a “+ new order” button) after the strip, staying fixed even when scrollable is true. TabsTrigger’s closable renders an accessible close control as a sibling of the tab (never nested inside it) and requires closeAccessibilityLabel; closing the currently selected tab moves selection to the previous sibling (else the next) via onValueChange, while closing a non-selected tab only calls onClose.

Prop Type Default Description
children React.ReactNode — Content rendered inside this element. The family’s composition section states which children it expects.
className string — Extra utility classes, merged after the component’s own via cn(...), so they win on conflict. An escape hatch for source-owned and application work, not a cross-engine portability guarantee.
value (required) string — Identifies this panel; it renders only while the parent Tabs’s value matches.

Also carries every prop of Omit<ViewProps, 'children' | 'role'> — that upstream contract is not reproduced here.

Prop Type Default Description
addon React.ReactNode — Extra content rendered after the tab strip (e.g. a pinned “+ new order” action). Stays fixed in place even when scrollable is true and the strip itself scrolls underneath it — it is never part of the horizontally-scrollable region.
className string — Extra utility classes, merged after the component’s own via cn(...), so they win on conflict. An escape hatch for source-owned and application work, not a cross-engine portability guarantee.
scrollable boolean false Renders the strip inside a horizontal scroll container instead of an equal-width flex row — each TabsTrigger sizes to its own content instead of stretching — and scrolls the selected TabsTrigger into view whenever the parent Tabs’s value changes. Existing (non-scrollable) tab strips are unaffected. Defaults to false.

Also carries every prop of Omit<ViewProps, 'accessibilityRole' | 'role'> — that upstream contract is not reproduced here.

Prop Type Default Description
children (required) React.ReactNode — Content rendered inside this element. The family’s composition section states which children it expects.
className string — Extra utility classes, merged after the component’s own via cn(...), so they win on conflict. An escape hatch for source-owned and application work, not a cross-engine portability guarantee.
disabled boolean false Disables every TabsTrigger inside, overriding each trigger’s own disabled. Defaults to false.
onValueChange (value: string) => void — Called with the pressed tab’s value when a non-selected TabsTrigger is pressed. Required for enabled usage (logs a dev warning otherwise).
value (required) string — The value of the currently active tab, matched against each TabsTrigger/TabsContent’s own value. Always controlled by the caller — there is no uncontrolled mode.

Also carries every prop of Omit<ViewProps, 'children'> — that upstream contract is not reproduced here.

Prop Type Default Description
children React.ReactNode — Content rendered inside this element. The family’s composition section states which children it expects.
className string — Extra utility classes, merged after the component’s own via cn(...), so they win on conflict. An escape hatch for source-owned and application work, not a cross-engine portability guarantee.
closable boolean false Renders an accessible close control as a sibling of this tab’s own pressable, never nested inside it (a Pressable-in-Pressable/<button>-in-<button> composition is the same anti-pattern flagged for DropdownMenuTrigger wrapping IconButton — nesting interactive elements breaks Web’s DOM validity and native’s hit-testing). Requires closeAccessibilityLabel. In a Web scrollable tablist the close sibling stays out of sequential Tab order so the tablist remains one roving composite; pressing Delete on the focused closable tab invokes the same close path. Defaults to false.
closeAccessibilityLabel string — Accessible name for the close control, e.g. `Close ${label}`. Required whenever closable is true — BeeUI does not synthesize an English default from the tab’s own label, since that would hardcode a locale. A dev warning fires if closable is set without it.
labelClassName string — Extra utility classes for the label text specifically, merged after the component’s own.
onClose (value: string) => void — Called with this tab’s value when its close control is pressed. If this tab is currently selected, Tabs’s onValueChange is also called — with the previous sibling’s value if one exists, else the next sibling’s — moving selection away from the tab being closed. No-op (besides onClose itself) when a non-selected tab closes, or when the closing tab has no remaining sibling.
value (required) string — Identifies this tab; compared against the parent Tabs’s value to determine whether it is selected.

Also carries every prop of Omit<PressableProps, 'accessibilityRole' | 'role' | 'children' | 'onPress'> — that upstream contract is not reproduced here.

The executable fixtures below are the source-grounded usage examples; consumers should not infer state ownership from DOM structure or another UI library.

  • No additional provider is required by this family. BeeUIProvider remains the recommended application root.
  • Peer/native dependencies visible to this Registry item: react, react-native
  • Registry dependency closure: core-cn, text, theme, use-required-callback-warning
  • Safe-area ownership remains explicit: shell surfaces touching system edges opt into SafeArea; components do not silently invent app-shell insets.
  • Web consumers load the BeeUI semantic theme CSS as documented in Web onboarding.

This family ships no platform-specific file, but its source branches on Platform, so some behavior differs by target.

The same public family is exposed across the supported target matrix; meaningful platform differences remain governed by the compatibility contract.

Evidence classes are not equal and this page does not blur them: Web behavior is exercised in a real browser, while iOS and Android carry package/export and native-compile evidence, which is not device-runtime proof. The compatibility contract records which class each claim rests on.

  • Roles this family assigns: button, tab, tablist, tabpanel — set in tabs.tsx by the components themselves, not by the caller.
  • Accessibility states and properties it sets: accessibilityLabel, disabled, selected — read from tabs.tsx.

Keyboard/focus behavior, announcements, Dynamic Type/Web zoom, RTL and reduced-motion expectations are not derived here — see Accessibility overview, Keyboard & focus, RTL/localization and Large text & zoom. BeeUI does not claim universal accessibility certification from automated tests.

  • Style axes: none of its own — its appearance comes from tokens and your own classes; it also carries Omit<PressableProps, 'accessibilityRole' | 'role' | 'children' | 'onPress'>, Omit<ViewProps, 'accessibilityRole' | 'role'>, Omit<ViewProps, 'children' | 'role'>, Omit<ViewProps, 'children'>.
  • Class-name surfaces: className, labelClassName.

Colors, spacing and typography come from semantic tokens rather than from values written here — see Theming and Density. A className is an escape hatch for source-owned and application work, not a cross-engine portability guarantee.

Each link below opens the Showcase at that exact example, not at the top of the gallery:

  • basic — smallest meaningful complete usage
  • states — the states example

The Showcase links demonstrate Web behavior; use the native-preview guide for real simulator/emulator/device paths.

This frame loads the real BeeUI Web Showcase on demand; it is not a second docs-only implementation. It proves browser behavior only. Use native preview for iOS/Android simulator, emulator or device paths.

  • Family root / primary export: Tabs
    • Public composition parts / helpers:
      • TabsContent
      • TabsList
      • TabsTrigger
    • Exported type surface:
      • TabsContentProps
      • TabsListProps
      • TabsProps
      • TabsTriggerProps

The tree above is ordinary document structure so it remains readable with keyboard and assistive technology; it is derived from the real public export family rather than a canvas-only diagram.

These are the parts of the typechecked runtime Showcase fixture behind this live preview — apps/showcase/component-gallery/component-gallery.tsx, 1103 lines — where Tabs is actually used: 12 lines in 1 place. Each block is copied verbatim from the line range named above it, so it is the same executable source, not a retelling of it. The rest of that file exercises other families and is not reproduced here.

Imports the examples below need (a filtered subset of the fixture’s own top-level imports):

import { Tabs, TabsContent, TabsList, TabsTrigger, Text } from '@beemvp/beeui-ui';
import * as React from 'react';

Fixture state this block reads (same file, line 463):

const [tab, setTab] = React.useState('overview');

lines 945–956:

<Tabs onValueChange={setTab} value={tab}>
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="details">Details</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<Text tone="muted">Overview content is mounted for the active tab.</Text>
</TabsContent>
<TabsContent value="details">
<Text tone="muted">Details content is mounted only when selected.</Text>
</TabsContent>
</Tabs>

Open the fixture itself for the full surrounding component. For a smaller app-specific example, start from the imports and fixture-state blocks above and keep only the state your screen owns.

Press handling is owned by the family: a trigger reaches onValueChange only when its tab is not already selected, so pressing the active tab does nothing and a trigger cannot carry its own press handler. closable without closeAccessibilityLabel warns in development — the close control has no other way to describe itself.