Skip to main content

Tabs

Corenavigation

Switch between peer views in the same context without navigating away.

import { Tabs } from "@elabs-ai/components-ui";
Install
npm install @elabs-ai/components-tokens @elabs-ai/components-ui

Skipping the @source line is the most common first failure: Tailwind does not scan node_modules, so every component renders unstyled. The one-time wiring

Prompt for your coding agent

Paste it into Claude Code, Cursor, Codex or any other coding agent. Text in [brackets] is yours to fill in.

Add the brand-ui Tabs component (from @elabs-ai/components-ui) to this project.

1. Read https://elabs-ai.com/llms.txt first: it maps every package and the rules. If you can use MCP servers, connect https://elabs-ai.com/mcp and use its search, docs and tokens tools. If not, run the same lookups in the terminal: `npx -y @elabs-ai/components-cli search "<what you need>"` and `npx -y @elabs-ai/components-cli docs <Component>`.
2. Look Tabs up before writing code: `npx -y @elabs-ai/components-cli docs Tabs` prints its real props, variants, what it pairs with and what to avoid.
3. If @elabs-ai/components-ui is not installed yet: `pnpm add @elabs-ai/components-tokens @elabs-ai/components-ui` (or `npm install @elabs-ai/components-tokens @elabs-ai/components-ui`). If this is the first brand-ui package in the project, wire it once: import `@elabs-ai/components-tokens/styles.css` plus the `themes/light.css` and `themes/dark.css` stylesheets in the CSS entry, add one Tailwind `@source` line per installed @elabs-ai package (Tailwind does not scan node_modules, so without it everything renders unstyled), and wrap the app root in `ThemeProvider` from `@elabs-ai/components-tokens`.
4. Use it here: [the screen or file, and what it should show]
5. Rules: use only @elabs-ai/components-* components and the semantic tokens — no raw colours, no raw font sizes, no arbitrary Tailwind values. Look up each component's real props with the docs tool before you use it; never guess a prop.
6. Done means: the typecheck passes, `npx -y @elabs-ai/components-cli audit src --strict` reports nothing, and you have opened the app and looked at every screen you touched in the light and the dark theme.

Avoid

  • Tabs for sequential steps — use a stepper/wizard; tabs are for peer, non-sequential views.
  • More than ~7 tabs — consider a Select or a sidebar nav.

Works with

Contains

  • TabsList
  • TabsTrigger
  • TabsContent

Examples

7 live examples

Overflow Scrollable

An 8-tab strip inside a 320px-wide container (phone-width) — narrower than the strip's natural width, so it must scroll rather than clip. Confirms every tab is reachable by keyboard: repeated `ArrowRight` walks focus/ selection all the way to the last tab, and the newly-active tab scrolls itself into the container's visible bounds (real layout — this assertion only means anything in a real browser, which is how `test-storybook` / `run-story-tests` execute it).

Wide Viewport Fits

Regression guard: a strip that FITS its container renders exactly as before — `justify-center-safe` behaves identically to `justify-center` when there's nothing to overflow, so no scrollbar appears — AND mounting it moves NOTHING. The tabs sit below a tall spacer inside a scroll region, out of view. The first cut of #344 called `Element.scrollIntoView()` on mount, which walks every scrollable ancestor: it scrolled this region (and the page) to reveal the tabs nobody had asked for. Asserting "no scrollbar" alone did not catch that — asserting the scroll POSITION does.

Programmatic Activation

The scroll-on-activation behaviour proved WITHOUT the browser's own focus-scrolling doing the work: a button beside the strip sets the value programmatically, so the newly-active tab is never focused and nothing but `TabsTrigger`'s own effect can bring it into view. The strip starts at scroll 0 (mount is not an activation), then moves — and only the strip moves.

Underline

`variant="underline"` — line tabs: a transparent row over a 1px rule, the active tab marked by a 2px `--primary` underline and foreground ink. Label weight follows the theme's `--control-weight`. Keyboard behaviour is Radix's, identical to the segmented default.

Rail

`variant="rail"` — vertical tabs for `<Tabs orientation="vertical">`: the list stacks, the active tab carries a start-edge `--primary` bar plus a muted fill, and a rich trigger (icon, title, one-line summary) wraps instead of truncating. Radix drives arrow-up/down between the tabs.

Token Driven Underline

Token-driven default (#392): NO `variant` prop on `TabsList` — an ancestor setting `--tabs-variant: underline` renders the exact same look as the explicit `Underline` story above, entirely via CSS (a theme could set this token instead of a region wrapper). Neither the list nor its triggers carry a `data-variant` attribute here.

API

TabsContent

Also accepts: ComponentPropsWithoutRef<typeof TabsPrimitive.Content>

TabsList

variant
segmented (default)underlinerail

Also accepts: ComponentPropsWithoutRef<typeof TabsPrimitive.List>, VariantProps<typeof tabsListVariants>

TabsTrigger

variant
segmented (default)underlinerail

Also accepts: ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>

Theming

The tokens each state resolves to. Change the token and every theme follows.

StateToken
listTabsList: bg-muted text-muted-foreground
activedata-[state=active]: bg-surface-elevated text-foreground shadow-sm
focusfocus-ring — the compound indicator: a ring-2 in --ring plus a 1px --ring-contour outline (#67) + ring-offset-2 ring-offset-background