RoadieRoadie
  • Home
  • Brand
  • Foundations
  • Content
  • Tokens
  • Components
  • Charts
  • Widgets
  • Appearance
HomeBrandFoundations
Appearance

Components

  • Navigation

    • Breadcrumb
    • LinkBeta
    • NavigatorAlpha
    • Steps
    • Tabs
  • Layout

    • Accordion
    • CollapsibleBeta
    • PaneAlpha
    • ScrollArea
    • Separator
  • Actions

    • Button
    • IconButton
    • ToggleBeta
    • Toggle groupBeta
    • ToolbarBeta
  • Fields

    • EditorAlpha
    • Field
    • Fieldset
    • Input
    • Label
    • Number fieldBeta
    • OTP fieldBeta
    • Query fieldAlpha
    • Textarea
  • Choices

    • Autocomplete
    • CheckboxBeta
    • Combobox
    • Radio group
    • Select
    • SliderBeta
    • SwitchBeta
  • Date and time

    • CalendarBeta
    • CalendarTile
    • Countdown
    • Dashboard periodBeta
    • Date fieldBeta
    • Date pickerBeta
    • Date range pickerBeta
    • DateTime
    • Duration
    • Time fieldBeta
  • Collections

    • Card
    • Carousel
    • DataTableBeta
    • ListAlpha
    • Marquee
    • RecordGridAlpha
    • RecordTableAlpha
    • SortableAlpha
    • TableBeta
  • Overlays

    • Dialog
    • Drawer
    • MenuBeta
    • Popover
    • ToastBeta
    • Tooltip
  • Status

    • Badge
    • CalloutBeta
    • DeltaBeta
    • Empty state
    • MeterBeta
    • ProgressBeta
    • SkeletonBeta
    • SparklineBeta
    • StatTileBeta
  • Media & brand

    • AvatarBeta
    • Icon Tile
    • Image
    • Logo
    • PatternBeta
    • QR codeBeta
    • SpotIllustration
  • Text

    • Code
    • Highlight
    • KbdBeta
    • Mark
    • Prose

Tabs

Status
Stable
Added
Components 2.5.028 Apr 2026
Updated
Components 2.16.020 Oct 2026
Docs updated
19 Oct 2026

Tabbed navigation between related views with an animated active indicator.

Import

import { Tabs } from '@oztix/roadie-components/tabs'

Examples

Default

<Tabs defaultValue='overview'>
<Tabs.List>
<Tabs.Tab value='overview'>Overview</Tabs.Tab>
<Tabs.Tab value='details'>Details</Tabs.Tab>
<Tabs.Tab value='history'>History</Tabs.Tab>
<Tabs.Indicator />

Emphasis

emphasis controls the visual treatment of the list and the indicator. The active indicator animates between tabs in every variant. Only the appearance of the indicator changes.

strong

A high-contrast segmented control with a solid inverted pill behind the active tab. Use when tabs need to stand out against a busy or photographic background.

<Tabs defaultValue='week' emphasis='strong'>
<Tabs.List>
<Tabs.Tab value='day'>Day</Tabs.Tab>
<Tabs.Tab value='week'>Week</Tabs.Tab>
<Tabs.Tab value='month'>Month</Tabs.Tab>
<Tabs.Indicator />

normal (default)

A segmented control with a raised pill that slides between active tabs. Use inside cards and toolbars.

<Tabs defaultValue='week' emphasis='normal'>
<Tabs.List>
<Tabs.Tab value='day'>Day</Tabs.Tab>
<Tabs.Tab value='week'>Week</Tabs.Tab>
<Tabs.Tab value='month'>Month</Tabs.Tab>
<Tabs.Indicator />

subtle

A ghost segmented control. It has no track, just a tinted pill that slides behind the active tab. Use when the surrounding surface already provides framing.

<Tabs defaultValue='week' emphasis='subtle'>
<Tabs.List>
<Tabs.Tab value='day'>Day</Tabs.Tab>
<Tabs.Tab value='week'>Week</Tabs.Tab>
<Tabs.Tab value='month'>Month</Tabs.Tab>
<Tabs.Indicator />

subtler

A flat row with a sliding underline. Use for in-page section navigation where tabs should defer to the surrounding content. Under forced colours the underline takes the system highlight colour.

<Tabs defaultValue='week' emphasis='subtler'>
<Tabs.List>
<Tabs.Tab value='day'>Day</Tabs.Tab>
<Tabs.Tab value='week'>Week</Tabs.Tab>
<Tabs.Tab value='month'>Month</Tabs.Tab>
<Tabs.Indicator />

Sizes

<Tabs defaultValue='week' size='sm'>
<Tabs.List>
<Tabs.Tab value='day'>Day</Tabs.Tab>
<Tabs.Tab value='week'>Week</Tabs.Tab>
<Tabs.Tab value='month'>Month</Tabs.Tab>
<Tabs.Indicator />

Icon only

A tab that holds only an icon becomes a circle at every size and emphasis, and the indicator follows its shape. With subtler, the underline runs the width of the circle. Give each one an aria-label, because the icon has no text for assistive tech to read. Roadie warns in development when a name is missing.

<Tabs defaultValue='tickets' emphasis='strong'>
<Tabs.List aria-label='Event views'>
<Tabs.Tab value='tickets' aria-label='Tickets'>
<TicketIcon weight='bold' className='size-4' />
</Tabs.Tab>
<Tabs.Tab value='attendees' aria-label='Attendees'>

Intents

intent cascades down to descendants via CSS custom properties. The active indicator picks up the intent's strong colour in subtler emphasis.

<Tabs defaultValue='one' intent='accent' emphasis='subtle'>
<Tabs.List>
<Tabs.Tab value='one'>Accent</Tabs.Tab>
<Tabs.Tab value='two'>Two</Tabs.Tab>
<Tabs.Indicator />
</Tabs.List>

States

Disabled tabs are skipped during keyboard navigation and ignore clicks.

<Tabs defaultValue='active'>
<Tabs.List>
<Tabs.Tab value='active'>Active</Tabs.Tab>
<Tabs.Tab value='disabled' disabled>Disabled</Tabs.Tab>
<Tabs.Tab value='another'>Another</Tabs.Tab>
<Tabs.Indicator />

Composition

With icons

Use the Base UI render prop to access the active state and switch a Phosphor icon's weight between bold and fill.

<Tabs defaultValue='profile' emphasis='normal'>
<Tabs.List>
<Tabs.Tab
value='profile'
render={(props, state) => (
<button {...props}>

Vertical direction

Pass direction='vertical' to render a column of left-aligned tabs. With emphasis='subtler' the active indicator becomes a 2px bar on the left edge that slides between tabs; the pill emphases (strong, normal, subtle) keep their full-rect shape around the active tab in vertical mode.

<Tabs defaultValue='one' direction='vertical' emphasis='subtler'>
<div className='flex gap-6'>
<Tabs.List>
<Tabs.Tab value='one'>Account</Tabs.Tab>
<Tabs.Tab value='two'>Notifications</Tabs.Tab>
<Tabs.Tab value='three'>Privacy</Tabs.Tab>

When space runs out

A horizontal list never wraps. When the tabs don't fit, the list scrolls sideways, and it scrolls the active tab into view.

<Tabs defaultValue='history' className='max-w-64'>
<Tabs.List>
<Tabs.Tab value='overview'>Overview</Tabs.Tab>
<Tabs.Tab value='sales'>Sales</Tabs.Tab>
<Tabs.Tab value='attendees'>Attendees</Tabs.Tab>
<Tabs.Tab value='history'>History</Tabs.Tab>

Persistent panels

By default, panels mount only when active. Pass keepMounted on Tabs.Panel to preserve in-flight state (forms, scroll position, video playback) when switching tabs.

<Tabs.Panel value='form' keepMounted>
<Field>
<Field.Label>Name</Field.Label>
<Field.Input />
</Field>
</Tabs.Panel>

Guidelines

  • Use Tabs for related views, not for top-level page navigation. Reach for Breadcrumb or a sidebar instead when tabs would otherwise represent separate pages.
  • Prefer emphasis='normal' inside cards or toolbars. The segmented track gives the control its own visual frame. Reach for emphasis='strong' when tabs need to stand out against a busy or photographic background, emphasis='subtle' for a quieter ghost-segmented look, and emphasis='subtler' for a flat underline-only row that defers to the surrounding content.
  • Place the Tabs.Indicator inside Tabs.List as a sibling of the tabs themselves, not inside a tab. The indicator is positioned absolutely against the list and reads its geometry from Base UI's --active-tab-* CSS variables.

Tabs switch panels; a toggle group sets a value

Use Tabs when each choice shows its own content below. When the choice changes how the same content is filtered or shown, such as a date range or a list or grid view, use a toggle group.

<Tabs.Tab value='overview'>Overview</Tabs.Tab>
<Tabs.Tab value='settings'>Settings</Tabs.Tab>

Do

Each tab opens its own panel with different content.

<Tabs.Tab value='7d'>7 days</Tabs.Tab>
<Tabs.Tab value='30d'>30 days</Tabs.Tab>

Don’t

Tabs that only change the range of one chart. There are no separate panels, so use a ToggleGroup.

Accessibility

  • Keyboard: Arrow keys move focus between tabs (ArrowLeft/ArrowRight for horizontal, ArrowUp/ArrowDown for vertical). Home and End jump to the first and last tab. Enter and Space activate the focused tab.
  • Active-on-focus: Pass activateOnFocus on Tabs.List to switch panels as focus moves, instead of waiting for Enter/Space.
  • ARIA: Base UI handles role='tablist', role='tab', role='tabpanel', aria-controls, aria-selected, and aria-orientation automatically.
  • Reduced motion: The indicator's slide animation is disabled under prefers-reduced-motion: reduce. The indicator still updates position, just without the transition.

API reference

Tabs

classNamestring | ((state: TabsRootState) => string)

CSS class applied to the element, or a function that returns a class based on the component's state.

defaultValueany

The default value. Use when the component is not controlled. When the value is `null`, no Tab will be active.

Defaults to 0.

valueany

The value of the currently active `Tab`. Use when the component is controlled. When the value is `null`, no Tab will be active.

onValueChange((value: any, eventDetails: TabsRootChangeEventDetails) => void)

Callback invoked when new value is being set. The event `reason` is `'none'` for user-initiated changes, such as a click or keyboard navigation; `'initial'` for the first automatic selection or fallback in uncontrolled roots when `defaultValue` is omitted or `undefined`, including when the implicit initial value is disabled or missing; `'disabled'` for automatic fallback when the selected tab becomes disabled in uncontrolled roots; or `'missing'` for automatic fallback when the selected tab is removed, or when an explicit `defaultValue` never matches a mounted tab in uncontrolled roots. For automatic changes, the selected value can be `null` when no enabled Tab is available as a fallback. Automatic changes cannot be canceled; calling `eventDetails.cancel()` for `'initial'`, `'disabled'`, or `'missing'` has no effect.

intent"neutral" | "brand" | "brand-secondary" | "brand-blue" | "brand-orange" | "brand-purple" | "accent" | "danger" | "success" | "warning" | "info"

Sets the intent cascade for descendant tabs.

emphasis"strong" | "normal" | "subtle" | "subtler"

Visual treatment for the list and indicator. See the emphasis ladder comment in `variants.ts` for the full description of each option.

Defaults to 'normal'.

size"sm" | "md" | "lg"

Tab height and typography size.

Defaults to 'md'.

direction"horizontal" | "vertical"

Layout flow direction. `vertical` stacks the tabs in a column. The active indicator follows the axis: in `subtler` emphasis the underline becomes a left-edge bar, while the pill emphases (`strong` / `normal` / `subtle`) keep their full-rect shape around the active tab.

Defaults to 'horizontal'.

Tabs.Indicator

classNamestring | ((state: TabsIndicatorState) => string)

CSS class applied to the element, or a function that returns a class based on the component's state.

Inherited from TabsIndicatorProps

renderBeforeHydrationboolean

Whether to render itself before React hydrates. This minimizes the time that the indicator isn't visible after server-side rendering.

Defaults to true.

Tabs.List

classNamestring | ((state: TabsListState) => string)

CSS class applied to the element, or a function that returns a class based on the component's state.

Inherited from TabsListProps

activateOnFocusboolean

Whether to automatically change the active tab on arrow key focus. Otherwise, tabs will be activated using <kbd>Enter</kbd> or <kbd>Space</kbd> key press.

Defaults to false.

loopFocusboolean

Whether to loop keyboard focus back to the first item when the end of the list is reached while using the arrow keys.

Defaults to true.

Tabs.Panel

classNamestring | ((state: TabsPanelState) => string)

CSS class applied to the element, or a function that returns a class based on the component's state.

Inherited from TabsPanelProps

valueany
Required

The value of the TabPanel. It will be shown when the Tab with the corresponding value is active.

keepMountedboolean

Whether to keep the HTML element in the DOM while the panel is hidden.

Defaults to false.

Tabs.Tab

classNamestring | ((state: TabsTabState) => string)

CSS class applied to the element, or a function that returns a class based on the component's state.

hrefstring

Render the tab as a routed anchor instead of a `<button>`. Useful for tabbed navigation where each tab maps to a route. Internal hrefs route through `RoadieLinkProvider`. Note: pressing Enter on a focused link-tab triggers native browser navigation immediately. For controlled `Tabs.value`, derive the value from the route (e.g. `usePathname()`) rather than from controlled local state — otherwise the active-tab indicator can flicker between selection and route change.

externalboolean

Force external-link treatment when `href` is set.

targetstring

Override the auto `target='_blank'` default on external hrefs.

Inherited from TabsTabProps

valueany
Required

The value of the Tab.

disabledboolean

Whether the Tab is disabled. If a first Tab on a `<Tabs.List>` is disabled, it won't initially be selected. Instead, the next enabled Tab will be selected. However, it does not work like this during server-side rendering, as it is not known during pre-rendering which Tabs are disabled. To work around it, ensure that `defaultValue` or `value` on `<Tabs.Root>` is set to an enabled Tab's value.

Inherited from NativeButtonProps

nativeButtonboolean

Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`).

Defaults to true.

Previous page← StepsNext pageAccordion →