View your dashboard overview.
Analyze your metrics.
Generate and view reports.
"use client"
import * as React from "react"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/registry/delta-ui/delta/tabs"
export function TabsDemo() {
const [activeTab, setActiveTab] = React.useState("overview")
return (
<div className="flex w-full max-w-lg flex-col gap-8">
<div className="space-y-2">
<Tabs
value={activeTab}
onValueChange={setActiveTab}
variant="underline"
>
<TabsList className="w-full">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
<TabsTrigger value="reports">Reports</TabsTrigger>
</TabsList>
<div className="relative min-h-[80px]">
<TabsContent
value="overview"
className="bg-card absolute inset-x-0 top-0 rounded-lg border p-4"
>
<p className="text-muted-foreground text-sm">
View your dashboard overview.
</p>
</TabsContent>
<TabsContent
value="analytics"
className="bg-card absolute inset-x-0 top-0 rounded-lg border p-4"
>
<p className="text-muted-foreground text-sm">
Analyze your metrics.
</p>
</TabsContent>
<TabsContent
value="reports"
className="bg-card absolute inset-x-0 top-0 rounded-lg border p-4"
>
<p className="text-muted-foreground text-sm">
Generate and view reports.
</p>
</TabsContent>
</div>
</Tabs>
</div>
</div>
)
}
Installation
pnpm dlx shadcn@latest add https://deltacomponents.dev/r/tabs.jsonnpm dlx shadcn@latest add https://deltacomponents.dev/r/tabs.jsonyarn dlx shadcn@latest add https://deltacomponents.dev/r/tabs.jsonbun dlx shadcn@latest add https://deltacomponents.dev/r/tabs.jsonCopy and paste the following code into your project.
"use client"
import * as React from "react"
import { LayoutGroup, motion } from "motion/react"
import { cn } from "@/lib/utils"
type TabVariant = "default" | "underline"
type TabSize = "sm" | "small" | "default" | "lg" | "large"
interface TabItem {
id: string
label: React.ReactNode
icon?: React.ReactNode
disabled?: boolean
}
interface TabsContextValue {
activeTab: string
setActiveTab: (id: string) => void
layoutId: string
variant: TabVariant
size: TabSize
indicatorThickness?: string
hoveredIndex: number | null
setHoveredIndex: (index: number | null) => void
tabRefs: React.MutableRefObject<(HTMLButtonElement | null)[]>
hoverStyle: { left: string; width: string }
animate?: boolean
}
const TabsContext = React.createContext<TabsContextValue | null>(null)
function useTabs() {
const context = React.useContext(TabsContext)
if (!context) {
throw new Error("useTabs must be used within an Tabs component")
}
return context
}
interface TabsProps {
defaultValue?: string
value?: string
onValueChange?: (value: string) => void
children: React.ReactNode
className?: string
variant?: TabVariant
size?: TabSize
/** Override the underline indicator thickness (e.g. "2px", "4px") */
indicatorThickness?: string
/** Enable animations for all tab content. Disabled by default for better performance. */
animate?: boolean
}
function Tabs({
defaultValue,
value,
onValueChange,
children,
className,
variant = "default",
size = "default",
indicatorThickness,
animate,
}: TabsProps) {
const [internalValue, setInternalValue] = React.useState(defaultValue ?? "")
const [hoveredIndex, setHoveredIndex] = React.useState<number | null>(null)
const [hoverStyle, setHoverStyle] = React.useState({
left: "0px",
width: "0px",
})
const tabRefs = React.useRef<(HTMLButtonElement | null)[]>([])
const layoutId = React.useId()
const activeTab = value ?? internalValue
const setActiveTab = React.useCallback(
(id: string) => {
if (value === undefined) {
setInternalValue(id)
}
onValueChange?.(id)
},
[value, onValueChange]
)
React.useEffect(() => {
if (hoveredIndex !== null && variant === "underline") {
const hoveredElement = tabRefs.current[hoveredIndex]
if (hoveredElement) {
const { offsetLeft, offsetWidth } = hoveredElement
setHoverStyle({
left: `${offsetLeft}px`,
width: `${offsetWidth}px`,
})
}
}
}, [hoveredIndex, variant])
return (
<TabsContext.Provider
value={{
activeTab,
setActiveTab,
layoutId,
variant,
size,
indicatorThickness,
hoveredIndex,
setHoveredIndex,
tabRefs,
hoverStyle,
animate,
}}
>
<LayoutGroup>
<div
data-slot="animated-tabs"
className={cn("flex flex-col gap-2", className)}
>
{children}
</div>
</LayoutGroup>
</TabsContext.Provider>
)
}
interface TabsListProps {
children: React.ReactNode
className?: string
}
function TabsList({ children, className }: TabsListProps) {
const { variant, size, hoveredIndex, hoverStyle } = useTabs()
const normalizedSize =
size === "small" ? "sm" : size === "large" ? "lg" : size
const listHeightClasses = {
sm: variant === "default" ? "h-8" : "h-8",
default: variant === "default" ? "h-10" : "h-10",
lg: variant === "default" ? "h-12" : "h-12",
}
const hoverHeightClasses = {
sm: "h-6",
default: "h-7",
lg: "h-9",
}
const hoverOffsetMap = {
sm: "-1px",
default: "-2px",
lg: "-3px",
}
return (
<div
data-slot="animated-tabs-list"
role="tablist"
className={cn(
"text-muted-foreground relative inline-flex items-center",
listHeightClasses[normalizedSize],
variant === "default" &&
"bg-muted w-fit justify-center rounded-[10px] p-1",
variant === "underline" &&
"border-border w-full justify-start gap-0 border-b",
className
)}
>
{variant === "underline" && (
<div
className={cn(
"bg-muted absolute z-0 rounded-md transition-all duration-300 ease-out",
hoverHeightClasses[normalizedSize]
)}
style={{
...hoverStyle,
opacity: hoveredIndex !== null ? 1 : 0,
top: `calc(50% + ${hoverOffsetMap[normalizedSize]})`,
transform: "translate3d(0, -50%, 0)",
willChange: 'transform, opacity, left, width',
}}
aria-hidden="true"
/>
)}
{children}
</div>
)
}
interface TabsTriggerProps {
value: string
children: React.ReactNode
className?: string
disabled?: boolean
icon?: React.ReactNode
}
function TabsTrigger({
value,
children,
className,
disabled = false,
icon,
}: TabsTriggerProps) {
const {
activeTab,
setActiveTab,
layoutId,
variant,
size,
indicatorThickness,
hoveredIndex,
setHoveredIndex,
tabRefs,
} = useTabs()
const isActive = activeTab === value
const indexRef = React.useRef<number>(-1)
const normalizedSize =
size === "small" ? "sm" : size === "large" ? "lg" : size
const defaultSizeClasses = {
sm: "h-6 px-2 py-1 text-xs",
default: "h-8 px-2.5 py-1.5 text-sm",
lg: "h-10 px-3 py-2 text-base",
}
const underlineSizeClasses = {
sm: "h-8 px-2 pb-2 pt-1.5 text-xs",
default: "h-10 px-3 pb-3 pt-2 text-sm",
lg: "h-12 px-4 pb-4 pt-2.5 text-base",
}
const setTabRef = React.useCallback(
(el: HTMLButtonElement | null) => {
if (el) {
const currentIndex = tabRefs.current.indexOf(el)
if (currentIndex === -1) {
indexRef.current = tabRefs.current.length
tabRefs.current.push(el)
} else {
indexRef.current = currentIndex
}
}
},
[tabRefs]
)
const underlineThicknessClasses = {
sm: "h-[2px]",
default: "h-[3px]",
lg: "h-[4px]",
}
return (
<button
ref={setTabRef}
type="button"
role="tab"
aria-selected={isActive}
aria-disabled={disabled}
disabled={disabled}
data-state={isActive ? "active" : "inactive"}
data-slot="animated-tabs-trigger"
onClick={() => !disabled && setActiveTab(value)}
onMouseEnter={() =>
variant === "underline" && setHoveredIndex(indexRef.current)
}
onMouseLeave={() => variant === "underline" && setHoveredIndex(null)}
className={cn(
"ring-offset-background relative z-10 inline-flex items-center justify-center gap-1.5 font-medium whitespace-nowrap",
"transition-all duration-200",
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
"disabled:pointer-events-none disabled:opacity-50",
variant === "default" && [
"rounded-md",
defaultSizeClasses[normalizedSize],
isActive
? "text-foreground"
: "text-muted-foreground hover:text-foreground/80",
],
variant === "underline" && [
"rounded-md",
underlineSizeClasses[normalizedSize],
isActive
? "text-foreground"
: "text-muted-foreground hover:text-foreground",
],
className
)}
>
{isActive && variant === "default" && (
<motion.div
layoutId={`${layoutId}-tab-indicator`}
initial={false}
className="bg-background absolute inset-0 rounded-md"
style={{
willChange: 'transform',
transform: 'translate3d(0, 0, 0)',
}}
transition={{
type: "spring",
duration: 0.4,
bounce: 0,
}}
/>
)}
{isActive && variant === "underline" && (
<motion.div
layoutId={`${layoutId}-tab-indicator`}
initial={false}
className={cn(
"bg-foreground absolute inset-x-0 bottom-0",
!indicatorThickness && underlineThicknessClasses[normalizedSize]
)}
style={{
willChange: 'transform',
transform: 'translate3d(0, 0, 0)',
...(indicatorThickness ? { height: indicatorThickness } : {}),
}}
transition={{
type: "spring",
duration: 0.4,
bounce: 0,
}}
/>
)}
<span className="relative z-10 flex items-center gap-1.5">
{icon && (
<span className="shrink-0 [&_svg]:pointer-events-none [&_svg]:size-4">
{icon}
</span>
)}
{children}
</span>
</button>
)
}
interface TabsContentProps {
value: string
children: React.ReactNode
className?: string
forceMount?: boolean
animateY?: number
animationDuration?: number
/** Enable animations for smooth tab transitions. Disabled by default for better performance with heavy content. */
animate?: boolean
}
function TabsContent({
value,
children,
className,
forceMount = false,
animateY,
animationDuration = 0.25,
animate,
}: TabsContentProps) {
const { activeTab, animate: contextAnimate } = useTabs()
const isActive = activeTab === value
// Use prop value if provided, otherwise fall back to context value, default to false
const shouldAnimate = animate !== undefined ? animate : contextAnimate ?? false
// When animations are disabled (default), use a regular div for better performance
if (!shouldAnimate) {
return (
<div
role="tabpanel"
data-state={isActive ? "active" : "inactive"}
data-slot="animated-tabs-content"
style={{
display: !isActive ? 'none' : 'block',
}}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className,
)}
tabIndex={isActive ? 0 : -1}
>
{children}
</div>
)
}
// When animations are enabled, use motion.div with GPU acceleration
return (
<motion.div
role="tabpanel"
data-state={isActive ? "active" : "inactive"}
data-slot="animated-tabs-content"
initial={false}
animate={{
opacity: isActive ? 1 : 0,
...(animateY !== undefined && { y: isActive ? 0 : animateY }),
}}
transition={{
duration: animationDuration,
ease: "easeInOut",
}}
style={{
willChange: animateY !== undefined ? 'transform, opacity' : 'opacity',
...(animateY !== undefined && { transform: 'translate3d(0, 0, 0)' }),
display: !isActive ? 'none' : 'block',
}}
className={cn(
"ring-offset-background focus-visible:ring-ring mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
className,
)}
tabIndex={isActive ? 0 : -1}
>
{children}
</motion.div>
)
}
interface TabsFromArrayProps {
tabs: TabItem[]
defaultValue?: string
value?: string
onValueChange?: (value: string) => void
className?: string
listClassName?: string
triggerClassName?: string
contentClassName?: string
children?: (tab: TabItem) => React.ReactNode
}
function TabsFromArray({
tabs,
defaultValue,
value,
onValueChange,
className,
listClassName,
triggerClassName,
contentClassName,
children,
}: TabsFromArrayProps) {
const initialValue = defaultValue ?? tabs[0]?.id
return (
<Tabs
defaultValue={initialValue}
value={value}
onValueChange={onValueChange}
className={className}
>
<TabsList className={listClassName}>
{tabs.map((tab) => (
<TabsTrigger
key={tab.id}
value={tab.id}
disabled={tab.disabled}
icon={tab.icon}
className={triggerClassName}
>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{children &&
tabs.map((tab) => (
<TabsContent key={tab.id} value={tab.id} className={contentClassName}>
{children(tab)}
</TabsContent>
))}
</Tabs>
)
}
export {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
TabsFromArray,
type TabItem,
type TabsProps,
type TabsListProps,
type TabsTriggerProps,
type TabsContentProps,
type TabsFromArrayProps,
}
Update the import paths to match your project setup.
Usage
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
export default function Example() {
return (
<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>
)
}Examples
Default Variant
The default variant features a background container with an animated indicator that slides between tabs.
Make changes to your account here.
Change your password here.
Manage your preferences.
"use client"
import * as React from "react"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/registry/delta-ui/delta/tabs"
export function TabsBackgroundDemo() {
const [activeTab, setActiveTab] = React.useState("account")
return (
<div className="flex w-full max-w-lg flex-col gap-8">
<div className="space-y-2">
<Tabs value={activeTab} onValueChange={setActiveTab} variant="default">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<div className="relative min-h-[80px]">
<TabsContent
value="account"
className="bg-card absolute inset-x-0 top-0 rounded-lg border p-4"
>
<p className="text-muted-foreground text-sm">
Make changes to your account here.
</p>
</TabsContent>
<TabsContent
value="password"
className="bg-card absolute inset-x-0 top-0 rounded-lg border p-4"
>
<p className="text-muted-foreground text-sm">
Change your password here.
</p>
</TabsContent>
<TabsContent
value="settings"
className="bg-card absolute inset-x-0 top-0 rounded-lg border p-4"
>
<p className="text-muted-foreground text-sm">
Manage your preferences.
</p>
</TabsContent>
</div>
</Tabs>
</div>
{/*
<div className="space-y-4">
<p className="text-muted-foreground text-xs">Size variants</p>
<div>
<p className="text-muted-foreground mb-2 text-xs">Small (sm)</p>
<Tabs defaultValue="tab1" size="sm">
<TabsList>
<TabsTrigger value="tab1">First</TabsTrigger>
<TabsTrigger value="tab2">Second</TabsTrigger>
<TabsTrigger value="tab3">Third</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div>
<p className="text-muted-foreground mb-2 text-xs">Default</p>
<Tabs defaultValue="tab1" size="default">
<TabsList>
<TabsTrigger value="tab1">First</TabsTrigger>
<TabsTrigger value="tab2">Second</TabsTrigger>
<TabsTrigger value="tab3">Third</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div>
<p className="text-muted-foreground mb-2 text-xs">Large (lg)</p>
<Tabs defaultValue="tab1" size="lg">
<TabsList>
<TabsTrigger value="tab1">First</TabsTrigger>
<TabsTrigger value="tab2">Second</TabsTrigger>
<TabsTrigger value="tab3">Third</TabsTrigger>
</TabsList>
</Tabs>
</div>
</div>
*/}
</div>
)
}
Underline Variant
The underline variant displays tabs with an animated underline indicator, perfect for navigation and section switching.
View your dashboard overview.
Analyze your metrics.
Generate and view reports.
"use client"
import * as React from "react"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/registry/delta-ui/delta/tabs"
export function TabsDemo() {
const [activeTab, setActiveTab] = React.useState("overview")
return (
<div className="flex w-full max-w-lg flex-col gap-8">
<div className="space-y-2">
<Tabs
value={activeTab}
onValueChange={setActiveTab}
variant="underline"
>
<TabsList className="w-full">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
<TabsTrigger value="reports">Reports</TabsTrigger>
</TabsList>
<div className="relative min-h-[80px]">
<TabsContent
value="overview"
className="bg-card absolute inset-x-0 top-0 rounded-lg border p-4"
>
<p className="text-muted-foreground text-sm">
View your dashboard overview.
</p>
</TabsContent>
<TabsContent
value="analytics"
className="bg-card absolute inset-x-0 top-0 rounded-lg border p-4"
>
<p className="text-muted-foreground text-sm">
Analyze your metrics.
</p>
</TabsContent>
<TabsContent
value="reports"
className="bg-card absolute inset-x-0 top-0 rounded-lg border p-4"
>
<p className="text-muted-foreground text-sm">
Generate and view reports.
</p>
</TabsContent>
</div>
</Tabs>
</div>
</div>
)
}
No Background
Remove the background from the default variant tabs for a cleaner, more minimal appearance.
Manage your account settings and preferences.
Change your password here.
Configure your application settings.
"use client"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/registry/delta-ui/delta/tabs"
export function TabsNoBackgroundDemo() {
return (
<Tabs defaultValue="account" variant="default" size="lg">
{/* Override container to have no background */}
<TabsList className="gap-1 bg-transparent p-0">
{/* Override active tab to use bg-muted instead of bg-background */}
<TabsTrigger
value="account"
className="data-[state=active]:[&>div]:bg-muted"
>
Account
</TabsTrigger>
<TabsTrigger
value="password"
className="data-[state=active]:[&>div]:bg-muted"
>
Password
</TabsTrigger>
<TabsTrigger
value="settings"
className="data-[state=active]:[&>div]:bg-muted"
>
Settings
</TabsTrigger>
</TabsList>
<div className="relative min-h-[80px]">
<TabsContent value="account" className="absolute inset-x-0 top-0">
<p className="text-muted-foreground text-sm">
Manage your account settings and preferences.
</p>
</TabsContent>
<TabsContent value="password" className="absolute inset-x-0 top-0">
<p className="text-muted-foreground text-sm">
Change your password here.
</p>
</TabsContent>
<TabsContent value="settings" className="absolute inset-x-0 top-0">
<p className="text-muted-foreground text-sm">
Configure your application settings.
</p>
</TabsContent>
</div>
</Tabs>
)
}
Size Variants
Tabs come in three sizes: sm, default, and lg. Choose the size that best fits your layout.
Large (lg)
Large size tabs for prominent navigation.
Change your password here.
Configure your settings.
"use client"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/registry/delta-ui/delta/tabs"
export function TabsSizesDemo() {
return (
<div className="flex w-full max-w-lg flex-col gap-8">
{/* Large size */}
<div className="space-y-2">
<p className="text-muted-foreground text-xs">Large (lg)</p>
<Tabs defaultValue="account" variant="default" size="lg">
<TabsList className="w-fit">
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<div className="relative min-h-[60px]">
<TabsContent value="account" className="absolute inset-x-0 top-0">
<p className="text-muted-foreground text-sm">
Large size tabs for prominent navigation.
</p>
</TabsContent>
<TabsContent value="password" className="absolute inset-x-0 top-0">
<p className="text-muted-foreground text-sm">
Change your password here.
</p>
</TabsContent>
<TabsContent value="settings" className="absolute inset-x-0 top-0">
<p className="text-muted-foreground text-sm">
Configure your settings.
</p>
</TabsContent>
</div>
</Tabs>
</div>
</div>
)
}
With Icons
Enhance your tabs with icons for better visual communication and user experience.
Default variant with icons
Manage your account settings and preferences.
Change your password here.
Configure your application settings.
Underline variant with icons (large size)
Manage your account settings and preferences.
Change your password here.
Configure your application settings.
"use client"
import { Lock, Settings, User } from "lucide-react"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/registry/delta-ui/delta/tabs"
export function TabsWithIconsDemo() {
return (
<div className="flex w-full max-w-lg flex-col gap-8">
{/* Default variant with icons */}
<div className="space-y-2">
<p className="text-muted-foreground text-xs">
Default variant with icons
</p>
<Tabs defaultValue="account" variant="default">
<TabsList>
<TabsTrigger value="account">
<User className="h-4 w-4" />
Account
</TabsTrigger>
<TabsTrigger value="password">
<Lock className="h-4 w-4" />
Password
</TabsTrigger>
<TabsTrigger value="settings">
<Settings className="h-4 w-4" />
Settings
</TabsTrigger>
</TabsList>
<div className="relative min-h-[60px]">
<TabsContent value="account" className="absolute inset-x-0 top-0">
<p className="text-muted-foreground text-sm">
Manage your account settings and preferences.
</p>
</TabsContent>
<TabsContent value="password" className="absolute inset-x-0 top-0">
<p className="text-muted-foreground text-sm">
Change your password here.
</p>
</TabsContent>
<TabsContent value="settings" className="absolute inset-x-0 top-0">
<p className="text-muted-foreground text-sm">
Configure your application settings.
</p>
</TabsContent>
</div>
</Tabs>
</div>
{/* Underline variant with icons */}
<div className="space-y-2">
<p className="text-muted-foreground text-xs">
Underline variant with icons (large size)
</p>
<Tabs defaultValue="account" variant="underline" size="lg">
<TabsList className="w-fit">
<TabsTrigger value="account">
<User className="h-5 w-5" />
Account
</TabsTrigger>
<TabsTrigger value="password">
<Lock className="h-5 w-5" />
Password
</TabsTrigger>
<TabsTrigger value="settings">
<Settings className="h-5 w-5" />
Settings
</TabsTrigger>
</TabsList>
<div className="relative min-h-[60px]">
<TabsContent value="account" className="absolute inset-x-0 top-0">
<p className="text-muted-foreground text-sm">
Manage your account settings and preferences.
</p>
</TabsContent>
<TabsContent value="password" className="absolute inset-x-0 top-0">
<p className="text-muted-foreground text-sm">
Change your password here.
</p>
</TabsContent>
<TabsContent value="settings" className="absolute inset-x-0 top-0">
<p className="text-muted-foreground text-sm">
Configure your application settings.
</p>
</TabsContent>
</div>
</Tabs>
</div>
</div>
)
}
Entrance Animations
Control how tab content appears with customizable entrance animations. Set animate={true} on TabsContent to enable smooth transitions powered by Framer Motion.
With Y transform (animateY=8)
Manage your account settings and preferences.
Change your password here.
Configure your application settings.
Enable animations at Tabs level (cleaner API)
Manage your account settings and preferences.
Change your password here.
Configure your application settings.
"use client"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/registry/delta-ui/delta/tabs"
export function TabsEntranceVariantDemo() {
return (
<div className="space-y-8">
{/* Y transform animation override */}
<div className="space-y-2">
<p className="text-muted-foreground text-sm">
With Y transform (animateY=8)
</p>
<Tabs defaultValue="account" variant="underline">
<TabsList className="w-fit">
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<div className="relative min-h-[80px]">
<TabsContent
value="account"
className="absolute inset-x-0 top-0"
animate
animateY={8}
>
<p className="text-muted-foreground text-sm">
Manage your account settings and preferences.
</p>
</TabsContent>
<TabsContent
value="password"
className="absolute inset-x-0 top-0"
animate
animateY={8}
>
<p className="text-muted-foreground text-sm">
Change your password here.
</p>
</TabsContent>
<TabsContent
value="settings"
className="absolute inset-x-0 top-0"
animate
animateY={8}
>
<p className="text-muted-foreground text-sm">
Configure your application settings.
</p>
</TabsContent>
</div>
</Tabs>
</div>
{/* Using Tabs-level animate prop */}
<div className="space-y-2">
<p className="text-sm text-muted-foreground">Enable animations at Tabs level (cleaner API)</p>
<Tabs defaultValue="account" variant="underline" animate>
<TabsList className="w-fit">
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<div className="relative min-h-[80px]">
<TabsContent value="account" className="absolute inset-x-0 top-0" animateY={8}>
<p className="text-sm text-muted-foreground">Manage your account settings and preferences.</p>
</TabsContent>
<TabsContent value="password" className="absolute inset-x-0 top-0" animateY={8}>
<p className="text-sm text-muted-foreground">Change your password here.</p>
</TabsContent>
<TabsContent value="settings" className="absolute inset-x-0 top-0" animateY={8}>
<p className="text-sm text-muted-foreground">Configure your application settings.</p>
</TabsContent>
</div>
</Tabs>
</div>
</div>
)
}
Note: Animations are disabled by default for optimal performance. Enable them by setting animate={true} on individual TabsContent components when you need smooth transitions for lightweight content.
API Reference
Tabs
The root tabs component that provides context for all child components.
| Prop | Type | Default | Description |
|---|---|---|---|
| defaultValue | string | - | The value of the tab that should be active when initially rendered |
| value | string | - | The controlled value of the tab to activate |
| onValueChange | function | - | Event handler called when the value changes |
| variant | "default" | "underline" | "default" | Visual style variant of the tabs |
| size | "sm" | "default" | "lg" | "default" | Size of the tabs |
| animate | boolean | false | Enable animations for all tab content (can be overridden per TabsContent) |
| className | string | - | Additional CSS classes |
TabsList
Container for the tab triggers with animated indicator support.
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | - | Additional CSS classes |
The TabsList component automatically inherits the variant and size from the parent Tabs component and positions tab triggers accordingly. For the underline variant, tabs align to the start; for the default variant, they're centered.
TabsTrigger
Individual tab trigger button that activates a tab panel when clicked.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | - | A unique value that associates the trigger with a content |
| disabled | boolean | false | When true, prevents the user from interacting with the tab |
| className | string | - | Additional CSS classes |
The trigger automatically displays the animated indicator when active. For the underline variant, the indicator appears as an animated underline; for the default variant, it appears as a background highlight.
TabsContent
Content panel associated with a tab trigger.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | - | A unique value that associates the content with a trigger |
| animate | boolean | inherited | Enable smooth animations (inherits from Tabs, can be overridden per content) |
| animationDuration | number | 0.25 | Animation duration in seconds (only applies when animate={true}) |
| animateY | number | - | Y-axis animation distance in pixels (only applies when animate={true}) |
| forceMount | boolean | false | Forces the content to always be mounted in the DOM |
| className | string | - | Additional CSS classes |
Performance Note: By default, animations are disabled for instant tab switching with zero overhead. This is recommended for most use cases, especially when dealing with heavy content.
You can enable animations in two ways:
- For all tabs: Set
animate={true}on theTabscomponent - Per tab: Set
animate={true}on individualTabsContentcomponents (overrides the Tabs setting)
TabsFromArray
Utility component for rendering tabs from an array of tab items, useful when working with dynamic data.
| Prop | Type | Default | Description |
|---|---|---|---|
| tabs | TabItem[] | - | Array of tab items to render |
| defaultValue | string | tabs[0] | Initial active tab value |
| value | string | - | Controlled active tab value |
| onValueChange | function | - | Event handler called when the value changes |
| className | string | - | Additional CSS classes for Tabs |
| listClassName | string | - | Additional CSS classes for TabsList |
| triggerClassName | string | - | Additional CSS classes for each TabsTrigger |
| contentClassName | string | - | Additional CSS classes for each TabsContent |
| children | (tab) => ReactNode | - | Render function for tab content |
TabItem Type:
interface TabItem {
id: string
label: React.ReactNode
icon?: React.ReactNode
disabled?: boolean
}Features
- Animated Indicators: Smooth spring animations powered by Framer Motion
- Two Variants: Choose between default (background) and underline styles
- Three Sizes: Small, default, and large options for different layouts
- Icon Support: Add icons to your tabs for enhanced visual communication
- Keyboard Navigation: Full keyboard support for accessibility
- Disabled State: Disable individual tabs when needed
- Responsive: Works seamlessly across all screen sizes
- Dark Mode: Automatically adapts to your theme
Accessibility
- Uses Radix UI Tabs primitive for full accessibility compliance
- Supports keyboard navigation (Arrow keys, Home, End)
- Proper ARIA attributes for screen readers
- Focus management and visual indicators
Notes
- The animated indicator uses Framer Motion's
layoutIdfeature for smooth transitions - Tab content supports controlled and uncontrolled state management
- The underline variant tabs align to the start by default for better UX
- The default variant centers tabs within their container