UI/UI
Tabs
A set of layered sections of content with smooth animations and modern styling.
<div className="flex flex-col items-center gap-6 w-full">
<Tabs
items={[
{ id: "home", label: "Home", icon: <Home /> },
{ id: "settings", label: "Settings", icon: <Settings /> },
{ id: "profile", label: "Profile", icon: <User /> },
]}
defaultValue="home"
/>
</div>
Installation
Install following dependencies:
npm install motion class-variance-authority
pnpm add motion class-variance-authority
yarn add motion class-variance-authority
bun add motion class-variance-authority
Copy and paste the following code into your project.
"use client";
import * as React from "react";
import { motion } from "motion/react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const tabsVariants = cva(
"relative inline-flex items-center justify-center rounded-lg transition-all duration-300 w-full",
{
variants: {
variant: {
default:
"bg-[hsl(var(--hu-background))] border border-[hsl(var(--hu-border))]",
ghost: "bg-transparent",
underline:
"bg-transparent border-b border-[hsl(var(--hu-border))] rounded-none",
},
size: {
sm: "h-9 p-1",
default: "h-10 p-1.5",
lg: "h-12 p-2",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
const tabTriggerVariants = cva(
"relative inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium transition-all duration-300 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--hu-ring))] focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 flex-1",
{
variants: {
variant: {
default:
"text-[hsl(var(--hu-muted-foreground))] hover:text-[hsl(var(--hu-foreground))] data-[state=active]:text-[hsl(var(--hu-primary-foreground))]",
ghost:
"text-[hsl(var(--hu-muted-foreground))] hover:text-[hsl(var(--hu-foreground))] hover:bg-[hsl(var(--hu-accent))] data-[state=active]:text-[hsl(var(--hu-primary-foreground))] data-[state=active]:bg-transparent",
underline:
"text-[hsl(var(--hu-muted-foreground))] hover:text-[hsl(var(--hu-foreground))] data-[state=active]:text-[hsl(var(--hu-accent-foreground))] rounded-none",
},
size: {
sm: "px-2.5 py-1 text-xs",
default: "px-3 py-1.5 text-sm",
lg: "px-4 py-2 text-base",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface TabItem {
id: string;
label: string;
icon?: React.ReactNode;
}
export interface TabsProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof tabsVariants> {
items: TabItem[];
defaultValue?: string;
value?: string;
onValueChange?: (value: string) => void;
indicatorColor?: string;
}
const Tabs = React.forwardRef<HTMLDivElement, TabsProps>(
(
{
className,
variant,
size,
items,
defaultValue,
value,
onValueChange,
indicatorColor = "hsl(var(--hu-accent))",
...props
},
ref,
) => {
const [activeValue, setActiveValue] = React.useState(
value || defaultValue || items[0]?.id,
);
const [activeTabBounds, setActiveTabBounds] = React.useState({
left: 0,
width: 0,
});
const tabRefs = React.useRef<(HTMLButtonElement | null)[]>([]);
React.useEffect(() => {
if (value !== undefined) {
setActiveValue(value);
}
}, [value]);
React.useEffect(() => {
const activeIndex = items.findIndex(
(item: TabItem) => item.id === activeValue,
);
const activeTab = tabRefs.current[activeIndex];
if (activeTab) {
const tabRect = activeTab.getBoundingClientRect();
const containerRect = activeTab.parentElement?.getBoundingClientRect();
if (containerRect) {
setActiveTabBounds({
left: tabRect.left - containerRect.left,
width: tabRect.width,
});
}
}
}, [activeValue, items]);
const handleTabClick = (tabId: string) => {
setActiveValue(tabId);
onValueChange?.(tabId);
};
return (
<div
ref={ref}
className={cn(tabsVariants({ variant, size }), className)}
{...props}
>
{" "}
{/* Animated indicator */}
<motion.div
className={cn(
"absolute z-10",
variant === "underline"
? "bottom-0 h-0.5 rounded-none"
: "top-1 bottom-1 rounded-md",
)}
style={{
backgroundColor:
variant === "underline"
? "hsl(var(--hu-foreground))"
: indicatorColor,
}}
initial={false}
animate={{
left: activeTabBounds.left,
width: activeTabBounds.width,
}}
transition={{
type: "spring",
stiffness: 400,
damping: 30,
}}
/>
{/* Tab triggers */}
{items.map((item: TabItem, index: number) => {
const isActive = activeValue === item.id;
return (
<button
key={item.id}
ref={(el) => {
tabRefs.current[index] = el;
}}
className={cn(
tabTriggerVariants({ variant, size }),
"relative z-20 text-[hsl(var(--hu-muted-foreground))] data-[state=active]:text-[hsl(var(--hu-accent-foreground))]",
)}
data-state={isActive ? "active" : "inactive"}
onClick={() => handleTabClick(item.id)}
type="button"
>
{item.icon && (
<span className="mr-2 [&_svg]:size-4">{item.icon}</span>
)}
{item.label}
</button>
);
})}
</div>
);
},
);
Tabs.displayName = "Tabs";
// Content component for tab panels
export interface TabsContentProps extends React.HTMLAttributes<HTMLDivElement> {
value: string;
activeValue?: string;
}
const TabsContent = React.forwardRef<HTMLDivElement, TabsContentProps>(
({ className, value, activeValue, children, ...props }, ref) => {
const isActive = value === activeValue;
if (!isActive) return null;
const {
onDrag,
onDragStart,
onDragEnd,
onAnimationStart,
onAnimationEnd,
onTransitionEnd,
...divProps
} = props;
return (
<motion.div
ref={ref}
className={cn(
"ring-offset-[hsl(var(--hu-background))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--hu-ring))] focus-visible:ring-offset-2",
className,
)}
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 4 }}
transition={{ duration: 0.2, ease: "easeOut" }}
{...divProps}
>
{children}
</motion.div>
);
},
);
TabsContent.displayName = "TabsContent";
export { Tabs, TabsContent, tabsVariants };
npx shadcn@latest add "https://ui.shadcn.com/docs/components/tabs"
pnpm dlx shadcn@latest add "https://ui.shadcn.com/docs/components/tabs"
yarn dlx shadcn@latest add "https://ui.shadcn.com/docs/components/tabs"
bun x shadcn@latest add "https://ui.shadcn.com/docs/components/tabs"
Usage
import { Tabs, TabsContent } from "@/components/ui/tabs";
import { TabsWithContent } from "@/components/ui/Tabs";
export default function MyComponent() {
return <TabsWithContent />;
}
Examples
Default
<Tabs
items={[
{ id: "account", label: "Account" },
{ id: "password", label: "Password" },
{ id: "settings", label: "Settings" },
]}
defaultValue="account"
/>
Variants
Default
Ghost
Underline
<Tabs variant="default" items={tabs} defaultValue="home" />
<Tabs variant="ghost" items={tabs} defaultValue="home" />
<Tabs variant="underline" items={tabs} defaultValue="home" />
With Icons
import { Home, Search, Bell, Heart } from "lucide-react";
<Tabs
items={[
{ id: "home", label: "Home", icon: <Home /> },
{ id: "search", label: "Search", icon: <Search /> },
{ id: "notifications", label: "Notifications", icon: <Bell /> },
{ id: "favorites", label: "Favorites", icon: <Heart /> },
]}
defaultValue="home"
/>
Sizes
Small
Default
Large
<Tabs size="sm" items={tabs} defaultValue="tab1" />
<Tabs size="default" items={tabs} defaultValue="tab1" />
<Tabs size="lg" items={tabs} defaultValue="tab1" />
``` </Tab>
</Tabs>
### Underline Variant
<Tabs items={["Preview", "Code"]}>
<Tab value="Preview">
<PreviewContainer>
<div className="w-full max-w-md mx-auto">
<TabsUnderlineExample />
</div>
</PreviewContainer>
</Tab>
<Tab value="Code">
```tsx
import { Tabs, TabsContent } from "@/components/ui/tabs";
import { Home, Settings, User, Bell } from "lucide-react";
function TabsUnderlineExample() {
const [activeTab, setActiveTab] = useState("home");
return (
<div className="w-full">
<Tabs
variant="underline"
items={[
{ id: "home", label: "Home", icon: <Home /> },
{ id: "notifications", label: "Notifications", icon: <Bell /> },
{ id: "profile", label: "Profile", icon: <User /> },
{ id: "settings", label: "Settings", icon: <Settings /> },
]}
value={activeTab}
onValueChange={setActiveTab}
/>
<TabsContent value="home" activeValue={activeTab}>
<div className="p-4 bg-accent/5 rounded-md mt-4">
<h3 className="font-semibold">Home Content</h3>
<p className="text-sm text-muted-foreground mt-2">
This is the home tab content with underline variant.
</p>
</div>
</TabsContent>
{/* Other TabsContent components... */}
</div>
);
}
With Content
Overview
Welcome to your dashboard overview. Here you can see a summary of your most important metrics and recent activity.
function TabsWithContent() {
const [activeTab, setActiveTab] = useState("overview");
return (
<div className="w-full max-w-md">
<Tabs
items={[
{ id: "overview", label: "Overview", icon: <Home /> },
{ id: "analytics", label: "Analytics", icon: <Search /> },
{ id: "settings", label: "Settings", icon: <Settings /> },
]}
value={activeTab}
onValueChange={setActiveTab}
/>
<div className="mt-4 p-4 border rounded-[var(--radius)]">
<TabsContent value="overview" activeValue={activeTab}>
<div className="space-y-2">
<h3 className="text-lg font-semibold">Overview</h3>
<p className="text-sm text-muted-foreground">
Welcome to your dashboard overview...
</p>
</div>
</TabsContent>
<TabsContent value="analytics" activeValue={activeTab}>
<div className="space-y-2">
<h3 className="text-lg font-semibold">Analytics</h3>
<p className="text-sm text-muted-foreground">
Detailed analytics and insights...
</p>
</div>
</TabsContent>
<TabsContent value="settings" activeValue={activeTab}>
<div className="space-y-2">
<h3 className="text-lg font-semibold">Settings</h3>
<p className="text-sm text-muted-foreground">
Configure your preferences...
</p>
</div>
</TabsContent>
</div>
</div>
);
}
Props
Tabs
Prop | Type | Default |
---|---|---|
className? | string | undefined |
indicatorColor? | string | "hsl(var(--hu-accent))" |
size? | "sm" | "default" | "lg" | "default" |
variant? | "default" | "ghost" | "underline" | "default" |
onValueChange? | (value: string) => void | undefined |
value? | string | undefined |
defaultValue? | string | items[0]?.id |
items? | TabItem[] | undefined |
TabsContent
Prop | Type | Default |
---|---|---|
className? | string | undefined |
activeValue? | string | undefined |
value? | string | undefined |
TabItem
Prop | Type | Default |
---|---|---|
icon? | React.ReactNode | undefined |
label? | string | undefined |
id? | string | undefined |
Edit on GitHub
Last updated on