Tabs

A set of layered sections of content—known as tab panels—that are displayed one at a time.

AI Overview

Understand the current landscape of artificial intelligence, trends, and applications across industries.

1'use client';
2
3import { Button } from '@/components/ui/button';
4import { Card } from '@/components/ui/card';
5import { Tabs } from '@/components/ui/tabs';
6
7export function Default() {
8 return (
9 <Tabs defaultValue="overview" variant="default" size="sm">
10 <Tabs.List>
11 <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
12 <Tabs.Trigger value="models">Models</Tabs.Trigger>
13 <Tabs.Trigger value="research">Research</Tabs.Trigger>
14 <Tabs.Trigger value="ethics">Ethics</Tabs.Trigger>
15 </Tabs.List>
16
17 <Tabs.Content value="overview">
18 <Card className="border-border shadow-sm">
19 <Card.Header>
20 <Card.Title>AI Overview</Card.Title>
21 <Card.Description>
22 Understand the current landscape of artificial intelligence, trends, and applications
23 across industries.
24 </Card.Description>
25 </Card.Header>
26 <Card.Content className="flex flex-col gap-3">
27 <div className="flex gap-3">
28 <Button onClick={() => alert('Viewing AI trends')} variant="secondary">
29 View Trends
30 </Button>
31 <Button variant="ghost" onClick={() => alert('Explore applications')}>
32 Explore Applications
33 </Button>
34 </div>
35 </Card.Content>
36 </Card>
37 </Tabs.Content>
38
39 <Tabs.Content value="models">
40 <Card className="border-border shadow-sm">
41 <Card.Header>
42 <Card.Title>AI Models</Card.Title>
43 <Card.Description>
44 Track the performance of different AI models, from NLP and computer vision to
45 reinforcement learning systems.
46 </Card.Description>
47 </Card.Header>
48 <Card.Content>
49 <Button onClick={() => alert('Opening model library')} variant="secondary">
50 View Models
51 </Button>
52 </Card.Content>
53 </Card>
54 </Tabs.Content>
55
56 <Tabs.Content value="research">
57 <Card className="border-border shadow-sm">
58 <Card.Header>
59 <Card.Title>Research & Publications</Card.Title>
60 <Card.Description>
61 Stay up-to-date with the latest research papers, case studies, and breakthroughs in AI
62 technology.
63 </Card.Description>
64 </Card.Header>
65 <Card.Content>
66 <Button onClick={() => alert('Browsing research papers')} variant="secondary">
67 Browse Research
68 </Button>
69 </Card.Content>
70 </Card>
71 </Tabs.Content>
72
73 <Tabs.Content value="ethics">
74 <Card className="border-border shadow-sm">
75 <Card.Header>
76 <Card.Title>Ethics & Governance</Card.Title>
77 <Card.Description>
78 Understand ethical considerations, regulations, and best practices for responsible AI
79 deployment.
80 </Card.Description>
81 </Card.Header>
82 <Card.Content className="flex gap-3">
83 <Button onClick={() => alert('View guidelines')} variant="secondary">
84 View Guidelines
85 </Button>
86 <Button variant="ghost" onClick={() => alert('Report concerns')}>
87 Report Concerns
88 </Button>
89 </Card.Content>
90 </Card>
91 </Tabs.Content>
92 </Tabs>
93 );
94}

Installation

Copy and paste the following code in your project.
'use client';
import { cva, type VariantProps } from 'class-variance-authority';
import { motion } from 'motion/react';
import * as React from 'react';
import { cn } from '../lib/cn';
// --- Animation constants (module level) ---
const TABS_INDICATOR_TRANSITION = { type: 'spring', bounce: 0.2, duration: 0.6 } as const;
// --- CVA ---
const tabsListVariants = cva(
'inline-flex rounded-md p-1 text-muted-foreground w-full sm:w-auto overflow-hidden',
{
variants: {
variant: {
default: 'bg-muted/50 border border-border/50 backdrop-blur-sm',
outline: 'border border-border bg-transparent',
underline: 'bg-transparent border-b border-border rounded-none p-0 justify-start w-full',
ghost: 'bg-transparent p-0 gap-2',
},
size: {
default: 'h-10',
sm: 'h-9',
lg: 'h-12',
},
},
},
);
const tabsTriggerVariants = cva(
'relative inline-flex items-center justify-center whitespace-nowrap px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 z-10 cursor-pointer',
{
variants: {
variant: {
default: 'text-muted-foreground data-[state=active]:text-foreground',
outline: 'text-muted-foreground data-[state=active]:text-foreground',
underline:
'text-muted-foreground hover:text-foreground data-[state=active]:text-foreground rounded-none bg-transparent px-4 pb-3 pt-2',
ghost:
'text-muted-foreground hover:text-foreground data-[state=active]:text-foreground hover:bg-muted/50 rounded-md',
},
size: {
default: 'text-sm',
sm: 'text-xs',
lg: 'text-base px-5',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
// --- Context ---
interface TabsContextValue {
activeTab: string;
setActiveTab: (value: string) => void;
direction: number;
setDirection: (dir: number) => void;
variant: NonNullable<TabsProps['variant']>;
layoutId: string;
}
const TabsContext = React.createContext<TabsContextValue | null>(null);
function useTabsContext() {
const context = React.use(TabsContext);
if (!context) throw new Error('Tabs components must be used within Tabs');
return context;
}
// --- Components ---
interface TabsProps extends React.HTMLAttributes<HTMLDivElement> {
defaultValue?: string;
value?: string;
onValueChange?: (value: string) => void;
variant?: 'default' | 'outline' | 'underline' | 'ghost';
size?: 'default' | 'sm' | 'lg';
}
const TabsRoot = ({
className,
defaultValue,
value: controlledValue,
onValueChange,
variant = 'default',
size = 'default',
children,
ref,
...props
}: TabsProps & { ref?: React.Ref<HTMLDivElement> }) => {
const [internalValue, setInternalValue] = React.useState(defaultValue || '');
const isControlled = controlledValue !== undefined;
const activeTab = isControlled ? controlledValue : internalValue;
const [direction, setDirection] = React.useState(0);
const layoutId = React.useId();
const setActiveTab = React.useCallback(
(newValue: string) => {
if (!isControlled) {
setInternalValue(newValue);
}
onValueChange?.(newValue);
},
[isControlled, onValueChange],
);
return (
<TabsContext
value={{
activeTab,
setActiveTab,
direction,
setDirection,
variant,
layoutId,
}}
>
<div ref={ref} className={cn('w-full', className)} {...props}>
{children}
</div>
</TabsContext>
);
};
TabsRoot.displayName = 'Tabs';
interface TabsListProps
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof tabsListVariants> {}
const TabsList = ({
className,
variant,
size,
children,
ref,
...props
}: TabsListProps & { ref?: React.Ref<HTMLDivElement> }) => {
const { variant: contextVariant } = useTabsContext();
const finalVariant = variant || contextVariant;
return (
<div
ref={ref}
role="tablist"
aria-orientation="horizontal"
className={cn(tabsListVariants({ variant: finalVariant, size }), className)}
{...props}
>
{children}
</div>
);
};
TabsList.displayName = 'TabsList';
interface TabsTriggerProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof tabsTriggerVariants> {
value: string;
children: React.ReactNode;
}
const TabsTrigger = ({
className,
value,
children,
variant,
size,
ref,
...props
}: TabsTriggerProps & { ref?: React.Ref<HTMLButtonElement> }) => {
const {
activeTab,
setActiveTab,
setDirection,
variant: contextVariant,
layoutId,
} = useTabsContext();
const isActive = activeTab === value;
const finalVariant = variant || contextVariant;
const buttonRef = React.useRef<HTMLButtonElement>(null);
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
const parent = buttonRef.current?.parentElement;
if (parent) {
const childrenArray = Array.from(parent.children);
const newIndex = childrenArray.indexOf(buttonRef.current!);
const currentIndex = childrenArray.findIndex(
(child) => child.getAttribute('data-state') === 'active',
);
if (currentIndex !== -1 && newIndex !== currentIndex) {
setDirection(newIndex > currentIndex ? 1 : -1);
}
}
setActiveTab(value);
props.onClick?.(e);
};
// Merge the external ref with our internal buttonRef
React.useEffect(() => {
if (!ref) return;
if (typeof ref === 'function') {
ref(buttonRef.current);
} else {
(ref as any).current = buttonRef.current;
}
}, [ref]);
return (
<button
ref={buttonRef}
type="button"
role="tab"
id={`${layoutId}-trigger-${value}`}
aria-selected={isActive}
aria-controls={`${layoutId}-content-${value}`}
tabIndex={isActive ? 0 : -1}
data-state={isActive ? 'active' : 'inactive'}
onClick={handleClick}
className={cn(tabsTriggerVariants({ variant: finalVariant, size }), className)}
{...props}
>
<span className="inherit relative z-20">{children}</span>
{isActive && (
<motion.div
layoutId={`${layoutId}-indicator`}
className={cn(
'absolute inset-0 z-10',
finalVariant === 'underline'
? 'bg-primary top-auto bottom-0 h-[2px] shadow-[0_0_10px_rgba(var(--primary),0.5)]'
: 'bg-secondary border-border/50 rounded-sm border shadow-sm',
)}
transition={TABS_INDICATOR_TRANSITION}
/>
)}
</button>
);
};
TabsTrigger.displayName = 'TabsTrigger';
interface TabsContentProps extends React.HTMLAttributes<HTMLDivElement> {
value: string;
}
const TabsContent = ({
className,
value,
children,
ref,
...props
}: TabsContentProps & { ref?: React.Ref<HTMLDivElement> }) => {
const { activeTab, layoutId } = useTabsContext();
const isActive = activeTab === value;
if (!isActive) return null;
return (
<div
ref={ref}
role="tabpanel"
id={`${layoutId}-content-${value}`}
aria-labelledby={`${layoutId}-trigger-${value}`}
tabIndex={0}
className={cn(
'ring-offset-background focus-visible:ring-ring mt-4 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
className,
)}
{...props}
>
{children}
</div>
);
};
TabsContent.displayName = 'TabsContent';
const Tabs = Object.assign(TabsRoot, {
List: TabsList,
Trigger: TabsTrigger,
Content: TabsContent,
});
export { Tabs };
Make sure to update the import paths according to your project structure.

Anatomy

import { Tabs } from '@/components/ui/tabs';
<Tabs defaultValue="account">
<Tabs.List>
<Tabs.Trigger value="account">Account</Tabs.Trigger>
<Tabs.Trigger value="password">Password</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="account">Account settings content</Tabs.Content>
<Tabs.Content value="password">Password settings content</Tabs.Content>
</Tabs>

Features

  • Multiple variants - Default, outline, underline, and ghost styles
  • Animated indicator - Smooth sliding active tab indicator
  • Keyboard navigation - Full keyboard support
  • Smooth transitions - Animated content switching

API Reference

Tabs

Root component that manages tab state.
PropTypeDefaultDescription
defaultValuestring-Default active tab
valuestring-Controlled active tab
onValueChange(value: string) => void-Callback when tab changes
variant'default' | 'outline' | 'underline' | 'ghost''default'Visual style
size'default' | 'sm' | 'lg''default'Tab size
classNamestring-Additional CSS classes

TabsList

Container for tab triggers.
PropTypeDefaultDescription
variant'default' | 'outline' | 'underline' | 'ghost'InheritsOverride variant
size'default' | 'sm' | 'lg'InheritsOverride size
classNamestring-Additional CSS classes

TabsTrigger

Individual tab button.
PropTypeDefaultDescription
valuestring-Unique tab identifier
variant'default' | 'outline' | 'underline' | 'ghost'InheritsOverride variant
size'default' | 'sm' | 'lg'InheritsOverride size
classNamestring-Additional CSS classes

TabsContent

Content panel for each tab.
PropTypeDefaultDescription
valuestring-Matching tab identifier
classNamestring-Additional CSS classes

Variants

Default

Tabs with a subtle background and border.

Outline

Tabs with a border outline style.

Underline

Tabs with an underline indicator for the active tab.

Ghost

Minimal tabs with no background, just hover effects.