Skip to main content

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

Loading diagram...

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

Loading diagram...

Keyboard Navigation

KeyDescription
ArrowRightMove focus to the next tab and select it (previous tab in RTL)
ArrowLeftMove focus to the previous tab and select it (next tab in RTL)
HomeMove focus to the first tab and select it
EndMove focus to the last tab and select it
TabMove 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:

ModePropWhen children mountWhen they unmountUse it for
Default-When the tab opensWhen the tab closesCheap panels where a fresh render is fine
Lazy, then keptrenderOnceThe first time the tab opensNever (hidden while inactive)Forms, editors, and previews whose state should survive tab switches
EagerforceMountOn first render, before the tab is ever openedNever (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>

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.

API Reference

Tabs

PropTypeDefaultDescription
valuestring-Controlled active tab value. Must be used alongside onValueChange
defaultValuestring-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>).
...propsOmit<React.HTMLProps<HTMLDivElement>, 'defaultValue'>-Additional props to spread to the content div

TabsList

PropTypeDefaultDescription
classNamestring-Additional CSS class names
childrenReact.ReactNode-TabsTrigger elements
...propsReact.HTMLProps<HTMLDivElement>-Additional props to spread to the container div (renders with role="tablist")

TabsTrigger

PropTypeDefaultDescription
valuestring(required)A unique value that identifies this tab trigger
defaultCheckedboolean-If true, sets this tab as the default active tab on first render
disabledboolean-Disables the tab trigger from user interaction
classNamestring-Additional CSS class names
childrenReact.ReactNode-Label or node to render inside the tab
...propsReact.HTMLProps<HTMLButtonElement>-Additional props to spread to the button element

TabsContent

PropTypeDefaultDescription
valuestring(required)The associated value that matches a TabsTrigger
renderOncebooleanfalseMount the children the first time the tab opens, then keep them mounted (hidden) so their state survives tab switches
forceMountbooleanfalseMount the children on first render, even before the tab is opened, and keep them mounted (hidden)
classNamestring-Additional CSS class names
childrenReact.ReactNode-Tab panel content
...propsReact.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.

PropTypeDefaultDescription
...propsTabsProps-All props from Tabs are supported

MotionTabsList

Replaces TabsList. Wraps with LayoutGroup for shared layoutId indicator animation. Requires the motion package.

PropTypeDefaultDescription
...propsTabsListProps-All props from TabsList are supported

MotionTabsTrigger

Replaces TabsTrigger. Adds sliding layoutId indicator and disabled tab shake feedback. Requires the motion package.

PropTypeDefaultDescription
...propsOmit<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.

PropTypeDefaultDescription
classNamestring-Additional CSS class names
childrenReact.ReactNode-MotionTabsContent elements
...propsOmit<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.

PropTypeDefaultDescription
valuestring(required)The associated value that matches a MotionTabsTrigger
classNamestring-Additional CSS class names
childrenReact.ReactNode-Tab panel content
...propsReact.HTMLProps<HTMLDivElement>-Additional props to spread to the content div