Smooth tabs
Tabs whose active pill slides between labels while panels crossfade with a directional blur.
API reference
Components
<SmoothTabs />
The tabs root: Base UI's Tabs.Root (selection semantics) held controlled so the slide direction can be computed before each change commits, plus the shared pill and the theme-timed transitions, provided through context. Renders a single wrapper element; lay its two children (a SmoothTabsList and a SmoothTabsPanels) out via className.
defaultValue?
The uncontrolled initial selected tab value. One of the child SmoothTabsTab/SmoothTabsPanel values. Ignored if value is passed.
value?
The selected tab value (controlled). Pair with onValueChange.
onValueChange?
Called with the newly selected tab's value whenever the selection changes (click or keyboard). Required for controlled use.
contentOffsetX?
Horizontal distance (px) the outgoing/incoming panel slides during the crossfade. The one genuine content-effect constant; timing (the indicator spring, the crossfade duration) is theme-owned and is not a prop.
className?
Merged onto the root wrapper - lay the tablist and panels out here (a flex flex-col gap-4 stack, a max-width).
children?
SmoothTabsList and SmoothTabsPanels.
<SmoothTabsList />
The tablist rail: Base UI's Tabs.List (roving-tabindex keyboard navigation; activateOnFocus makes selection follow focus - automatic activation) dressed as a recessed bg-muted well.
ariaLabel?
Accessible name for the role="tablist" (e.g. "Workspace views"). Strongly recommended - a tablist with no label is opaque to screen-reader users.
className?
Merged onto the tablist shell - extend the default recessed bg-muted rail here.
children?
The SmoothTabsTab children.
<SmoothTabsTab />
One tab: Base UI's Tabs.Tab (a real role="tab" button with the aria-selected/aria-controls wiring and roving tabindex) timed by the theme's snap colour transition, carrying the shared sliding pill when it is selected. Must be rendered inside a SmoothTabsList.
value
This tab's value. When it matches the group's selected value the tab is active and carries the sliding pill. Must match a SmoothTabsPanel.
children?
The tab's label (and any inline content).
className?
Merged onto the tab <button>.
<SmoothTabsPanels />
The crossfade viewport: clips the horizontally-sliding panels (overflow-hidden) and drives a directional AnimatePresence mode="sync" swap stacked in one CSS grid cell. Reads its SmoothTabsPanel children as config, and renders the single active one as a real Base UI Tabs.Panel (role/aria wired by the primitive) rendered as the animated motion.div. keepMounted + [&[hidden]]:block keep the outgoing panel visible while its exit animation runs.
children?
The SmoothTabsPanel children. Only the one matching the selected value is shown; enter/exit run together in a shared grid cell so the card height holds when panels are the same size.
className?
Merged onto the crossfade viewport - give it its card surface, border and a min-h-* so a taller panel does not leave a short one looking collapsed.
<SmoothTabsPanel />
A declarative panel: it holds a value and its content, and renders nothing itself. Its parent SmoothTabsPanels reads these props and renders the single active panel inside the animated role="tabpanel" wrapper, so the crossfade is one shared element across every tab. Must be a child of SmoothTabsPanels.
value
This panel's value. It is shown when it matches the group's selected value; must match a SmoothTabsTab.
children?
The panel's content.
className?
Merged onto the animated role="tabpanel" wrapper SmoothTabsPanels renders for the active panel (its padding, its inner layout).
Hooks
useSmoothTabsContext
Public hook: read the active tab value and a selector, for a consumer that wants to drive or reflect the tabs from outside a part (a "next" button, a breadcrumb that mirrors the active view). Must be called inside a <SmoothTabs>.
useSmoothTabsContext(): {
/** The currently selected tab's value. */
value: string
/** Select a tab by value (drives the same slide + direction a click does). */
select: (value: string) => void
}