tabs
A set of layered sections of content - known as tab panels - that are displayed one at a time.
Philosophy
Tabs organize content into parallel views - only one is visible at a time, but all are equally important. We keep the component controlled-optional (works with or without state management) because simple use cases shouldn't pay the complexity tax. The TabsList/TabsTrigger/TabsContent split keeps styling separate from behavior.
How It's Built
Installation
npx @gentleduck/cli add tabs
npx @gentleduck/cli add tabs
Usage
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
<Tabs
defaultValue="account"
className="w-[400px]"
>
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
</TabsList>
<TabsContent value="account">Make changes to your account here.</TabsContent>
<TabsContent value="password">Change your password here.</TabsContent>
</Tabs>
Component Composition
Keyboard Navigation
| Key | Description |
|---|---|
ArrowRight | Move focus to the next tab and select it (previous tab in RTL) |
ArrowLeft | Move focus to the previous tab and select it (next tab in RTL) |
Home | Move focus to the first tab and select it |
End | Move focus to the last tab and select it |
Tab | Move focus onto the active tab, then out of the tab list |
Keeping Panels Mounted
By default, TabsContent renders its children only while its tab is active. Switching away unmounts them, so form input, scroll position, and fetched data reset. Two props change that:
| Mode | Prop | When children mount | When they unmount | Use it for |
|---|---|---|---|---|
| Default | - | When the tab opens | When the tab closes | Cheap panels where a fresh render is fine |
| Lazy, then kept | renderOnce | The first time the tab opens | Never (hidden while inactive) | Forms, editors, and previews whose state should survive tab switches |
| Eager | forceMount | On first render, before the tab is ever opened | Never (hidden while inactive) | Panels that must be in the DOM up front, such as content you want indexed or measured |
Inactive panels that stay mounted get display: none and aria-hidden, so they stay out of the layout and the accessibility tree.
Type in the Draft tab, switch to Activity, and come back: the draft keeps its text and its mount time, while Activity shows a new mount time on every visit.
<TabsContent renderOnce value="draft">
<Textarea />
</TabsContent>
renderOnce costs nothing until the tab is opened, then keeps that panel's DOM and React state alive. With many heavy panels, prefer the default and lift the state you want to keep into the parent.RTL Support
Set the direction once with DirectionProvider (or <html dir="rtl">) and the tabs follow it: the list mirrors, arrow keys move in the visual direction, and motion content slides from the matching side. No dir prop or rtl: classes needed.
Motion
Use MotionTabs, MotionTabsList, MotionTabsTrigger, and MotionTabsContent for a sliding active indicator and direction-aware content crossfade powered by motion. The indicator slides between tabs via layoutId and content panels crossfade with a directional shift, a slight scale-down, and blur.
Segmented Control
A pill-shaped segmented control with a sliding indicator and disabled tab support.
motion package. Replace Tabs with MotionTabs, TabsList with MotionTabsList, TabsTrigger with MotionTabsTrigger, and TabsContent with MotionTabsContent. Wrap content panels in MotionTabsContents for coordinated directional slide animations.API Reference
Tabs
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | Controlled active tab value. Must be used alongside onValueChange |
defaultValue | string | - | Uncontrolled initial tab value |
onValueChange | (value: string) => void | - | Callback fired when the active tab changes |
dir | 'ltr' | 'rtl' | - | Text direction override. Resolved via useDirection (dir prop -> DirectionProvider -> <html dir>). |
...props | Omit<React.HTMLProps<HTMLDivElement>, 'defaultValue'> | - | Additional props to spread to the content div |
TabsList
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS class names |
children | React.ReactNode | - | TabsTrigger elements |
...props | React.HTMLProps<HTMLDivElement> | - | Additional props to spread to the container div (renders with role="tablist") |
TabsTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | (required) | A unique value that identifies this tab trigger |
defaultChecked | boolean | - | If true, sets this tab as the default active tab on first render |
disabled | boolean | - | Disables the tab trigger from user interaction |
className | string | - | Additional CSS class names |
children | React.ReactNode | - | Label or node to render inside the tab |
...props | React.HTMLProps<HTMLButtonElement> | - | Additional props to spread to the button element |
TabsContent
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | (required) | The associated value that matches a TabsTrigger |
renderOnce | boolean | false | Mount the children the first time the tab opens, then keep them mounted (hidden) so their state survives tab switches |
forceMount | boolean | false | Mount the children on first render, even before the tab is opened, and keep them mounted (hidden) |
className | string | - | Additional CSS class names |
children | React.ReactNode | - | Tab panel content |
...props | React.HTMLProps<HTMLDivElement> | - | Additional props to spread to the content div |
MotionTabs
Replaces Tabs. Tracks tab order for directional content animation. Uses domMax for layout animations. Requires the motion package.
| Prop | Type | Default | Description |
|---|---|---|---|
...props | TabsProps | - | All props from Tabs are supported |
MotionTabsList
Replaces TabsList. Wraps with LayoutGroup for shared layoutId indicator animation. Requires the motion package.
| Prop | Type | Default | Description |
|---|---|---|---|
...props | TabsListProps | - | All props from TabsList are supported |
MotionTabsTrigger
Replaces TabsTrigger. Adds sliding layoutId indicator and disabled tab shake feedback. Requires the motion package.
| Prop | Type | Default | Description |
|---|---|---|---|
...props | Omit<TabsTriggerProps, 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart'> | - | All props from TabsTrigger except motion event handlers |
MotionTabsContents
Wrapper around all MotionTabsContent panels. Provides a single AnimatePresence with overflow-hidden for directional slide animation. Requires the motion package.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS class names |
children | React.ReactNode | - | MotionTabsContent elements |
...props | Omit<React.HTMLProps<HTMLDivElement>, 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart'> | - | Additional props to spread to the container div, except motion event handlers |
MotionTabsContent
Individual tab panel inside MotionTabsContents. Renders content with proper ARIA attributes. Requires the motion package.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | (required) | The associated value that matches a MotionTabsTrigger |
className | string | - | Additional CSS class names |
children | React.ReactNode | - | Tab panel content |
...props | React.HTMLProps<HTMLDivElement> | - | Additional props to spread to the content div |