Components
Base Components
Tabs
A set of layered sections (tab panels) that display one panel at a time, with triggers to switch between them.
Example
Make changes to your account here.
Installation
Install the following dependencies:
npm install class-variance-authorityCreate a tabs.tsx file and paste the following code into it.
"use client"import * as React from "react"import { cva, type VariantProps } from "class-variance-authority"import { ChevronDown } from "lucide-react"import { Tabs as TabsPrimitive } from "radix-ui"import { cn } from "@/lib/utils"import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,} from "@/components/ui/dropdown-menu"const TabsContext = React.createContext<{ value?: string onValueChange?: (value: string) => void}>({})function Tabs({ className, orientation = "horizontal", value: controlledValue, defaultValue, onValueChange, ...props}: React.ComponentProps<typeof TabsPrimitive.Root>) { const [value, setValue] = React.useState(controlledValue || defaultValue) React.useEffect(() => { if (controlledValue !== undefined) { setValue(controlledValue) } }, [controlledValue]) const handleValueChange = React.useCallback( (val: string) => { if (controlledValue === undefined) { setValue(val) } onValueChange?.(val) }, [controlledValue, onValueChange] ) return ( <TabsContext.Provider value={{ value, onValueChange: handleValueChange }} > <TabsPrimitive.Root data-slot="tabs" data-orientation={orientation} orientation={orientation} value={value} onValueChange={handleValueChange} className={cn( "group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className )} {...props} /> </TabsContext.Provider> )}const tabsListVariants = cva( "rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col", { variants: { variant: { default: "bg-surface w-fit", line: "gap-1 bg-transparent w-fit", overflow: "bg-transparent w-full justify-start", }, }, defaultVariants: { variant: "default", }, })function TabsList({ className, variant = "default", children, ...props}: React.ComponentProps<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>) { const { value, onValueChange } = React.useContext(TabsContext) if (variant === "overflow") { const tabs = React.Children.map(children, (child) => { if ( React.isValidElement(child) && typeof (child.props as { value?: string }).value === "string" ) { return { value: (child.props as { value: string }).value, label: (child.props as { children?: React.ReactNode }).children, disabled: (child.props as { disabled?: boolean }).disabled, } } return null })?.filter(Boolean) const activeTab = tabs?.find((tab) => tab?.value === value) return ( <div className={cn("flex items-center w-full", className)}> <DropdownMenu> <DropdownMenuTrigger asChild> <button className={cn( "flex items-center gap-2 px-3 py-1.5 text-sm font-medium border rounded-md bg-background hover:bg-muted/50 transition-colors outline-none", "focus-visible:ring-2 focus-visible:ring-ring/50" )} > <span className="truncate">{activeTab?.label || "Select tab"}</span> <ChevronDown className="size-4 opacity-50 shrink-0" /> </button> </DropdownMenuTrigger> <DropdownMenuContent align="start" className="w-[200px]"> {tabs?.map((tab) => ( <DropdownMenuItem key={tab?.value} disabled={tab?.disabled} onSelect={() => onValueChange?.(tab?.value)} className={cn( tab?.value === value && "bg-muted font-semibold" )} > {tab?.label} </DropdownMenuItem> ))} </DropdownMenuContent> </DropdownMenu> </div> ) } return ( <TabsPrimitive.List data-slot="tabs-list" data-variant={variant} className={cn(tabsListVariants({ variant }), className)} {...props} > {children} </TabsPrimitive.List> )}const TabsTrigger = function TabsTrigger({ className, ...props}: React.ComponentProps<typeof TabsPrimitive.Trigger>) { return ( <TabsPrimitive.Trigger data-slot="tabs-trigger" className={cn( "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent", "data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground", "after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100", className )} {...props} /> )}TabsTrigger.displayName = "TabsTrigger"function TabsContent({ className, ...props}: React.ComponentProps<typeof TabsPrimitive.Content>) { return ( <TabsPrimitive.Content data-slot="tabs-content" className={cn("flex-1 outline-none", className)} {...props} /> )}export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }Check the import paths to ensure they match your project setup.
Usage
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
} from "@/components/ui/tabs";<Tabs defaultValue="tab1" value={value} onValueChange={setValue}>
<TabsList>
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
</TabsList>
<TabsContent value="tab1">Content for tab 1</TabsContent>
<TabsContent value="tab2">Content for tab 2</TabsContent>
</Tabs>Tabs as the root with defaultValue (uncontrolled) or value and onValueChange (controlled). Each TabsTrigger and TabsContent must have a matching value. TabsList supports a variant of "default" (pill background) or "line".
Examples
Default
Overview content.
Line variant
Usevariant="line" on TabsList for an underline-style tab list.
Content for tab one.
Vertical orientation
Setorientation="vertical" on Tabs to stack the list and content side by side.
General settings.
Disabled trigger
Disable a single tab with thedisabled prop on TabsTrigger.
This tab is active.
Overflow variant
Theoverflow variant transforms the tab list into a dropdown menu. This is useful for compact layouts where a full list of triggers might not fit.
Overview content.
Controlled
Control the active tab withvalue and onValueChange on Tabs.
const [value, setValue] = React.useState("tab1");
<Tabs value={value} onValueChange={setValue}>
...
</Tabs>API Reference
Tabs
The root component that manages tab state. Supportsvalue, defaultValue, onValueChange, and orientation ("horizontal" | "vertical").
asChildboolean
Default: -
| Prop | Type | Default | Description |
|---|---|---|---|
| asChild | boolean | - | - |
TabsList
Container for the tab triggers. Use thevariant prop for "default" (pill) or "line" styling.
asChildboolean
Default: -
variant"line" | "default" | "overflow" | null
Default: default
| Prop | Type | Default | Description |
|---|---|---|---|
| asChild | boolean | - | - |
| variant | "line" | "default" | "overflow" | null | default | - |
TabsTrigger
The button that switches to a tab. Each trigger must have a uniquevalue that matches a TabsContent panel.
asChildboolean
Default: -
disabledboolean
Default: -
type"button" | "submit" | "reset"
Default: -
| Prop | Type | Default | Description |
|---|---|---|---|
| asChild | boolean | - | - |
| disabled | boolean | - | - |
| type | "button" | "submit" | "reset" | - | - |
TabsContent
The panel shown when its tab is active. Each content must have avalue matching a TabsTrigger.
asChildboolean
Default: -
| Prop | Type | Default | Description |
|---|---|---|---|
| asChild | boolean | - | - |