A set of layered sections of content that are displayed one at a time.
Make changes to your account here. Click save when you're done.
"use client";
import { Button } from "@/components/ui/button/button";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs/tabs";
import styles from "./tabs-demo.module.css";
const tabs = [
{ id: "account", label: "Account" },
{ id: "password", label: "Password" },
{ id: "team", label: "Team" },
];
export default function TabsBasic() {
return (
<div className={styles.container}>
<Tabs defaultValue="account">
<TabsList>
{tabs.map((tab) => (
<TabsTrigger key={tab.id} value={tab.id}>
{tab.label}
</TabsTrigger>
))}
</TabsList>
<TabsContent value="account">
<div className={styles.tabContent}>
<div>
<h3 className={styles.heading}>Account Settings</h3>
<p className={styles.description}>
Make changes to your account here. Click save when you're
done.
</p>
</div>
<div className={styles.buttonGroup}>
<Button size="sm">Save Changes</Button>
<Button size="sm" variant="outline">
Cancel
</Button>
</div>
</div>
</TabsContent>
<TabsContent value="password">
<div className={styles.tabContent}>
<div>
<h3 className={styles.heading}>Password</h3>
<p className={styles.description}>
Change your password here. After saving, you'll be logged
out.
</p>
</div>
<div className={styles.buttonGroup}>
<Button size="sm">Update Password</Button>
<Button size="sm" variant="outline">
Cancel
</Button>
</div>
</div>
</TabsContent>
<TabsContent value="team">
<div className={styles.tabContent}>
<div>
<h3 className={styles.heading}>Team Management</h3>
<p className={styles.description}>
Invite and manage your team members here.
</p>
</div>
<div className={styles.buttonGroup}>
<Button size="sm">Invite Member</Button>
<Button size="sm" variant="outline">
Manage Roles
</Button>
</div>
</div>
</TabsContent>
</Tabs>
</div>
);
}
npx shadcn@latest add https://roiui.com/r/tabs.jsonMake changes to your account here. Click save when you're done.
"use client";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
import useMeasure from "react-use-measure";
import { Button } from "@/components/ui/button/button";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs/tabs";
import styles from "./tabs-motion.module.css";
const SLIDE_OFFSET_PERCENTAGE = 110;
const tabs = [
{
id: "account",
label: "Account",
heading: "Account Settings",
description:
"Make changes to your account here. Click save when you're done.",
buttons: [
{ label: "Save Changes", variant: "primary" as const },
{ label: "Cancel", variant: "outline" as const },
],
},
{
id: "password",
label: "Password",
heading: "Password",
description:
"Change your password here. After saving, you'll be logged out.",
buttons: [
{ label: "Update Password", variant: "primary" as const },
{ label: "Cancel", variant: "outline" as const },
],
},
{
id: "team",
label: "Team",
heading: "Team Management",
description:
"Invite and manage your team members. Set permissions, roles, and access levels for each team member.",
buttons: [
{ label: "Invite Member", variant: "primary" as const },
{ label: "Manage Roles", variant: "outline" as const },
],
},
{
id: "billing",
label: "Billing",
heading: "Billing",
description: "View your billing information and payment methods.",
buttons: [
{ label: "Save Changes", variant: "primary" as const },
{ label: "Cancel", variant: "outline" as const },
],
},
];
const contentVariants = {
initial: (direction: number) => ({
x: `${SLIDE_OFFSET_PERCENTAGE * direction}%`,
opacity: 0,
filter: "blur(4px)",
}),
active: { x: "0%", opacity: 1, filter: "blur(0px)" },
exit: (direction: number) => ({
x: `${-SLIDE_OFFSET_PERCENTAGE * direction}%`,
opacity: 0,
filter: "blur(4px)",
}),
};
const contentTransition = {
type: "spring" as const,
bounce: 0.1,
duration: 0.5,
};
export default function TabsFramerMotion() {
const [activeTab, setActiveTab] = useState("account");
const [direction, setDirection] = useState(0);
const [ref, bounds] = useMeasure();
const handleTabChange = (newTab: string) => {
const currentIndex = tabs.findIndex((t) => t.id === activeTab);
const newIndex = tabs.findIndex((t) => t.id === newTab);
setDirection(newIndex > currentIndex ? 1 : -1);
setActiveTab(newTab);
};
const activeTabData = tabs.find((tab) => tab.id === activeTab);
return (
<div className={styles.container}>
<Tabs onValueChange={handleTabChange} value={activeTab}>
<TabsList className={styles.tabsList}>
{tabs.map((tab) => (
<TabsTrigger
className={styles.tabTrigger}
data-framer-motion="true"
key={tab.id}
tabIndex={0}
value={tab.id}
>
{activeTab === tab.id && (
<motion.span
className={styles.bubble}
layoutId="bubble"
style={{ borderRadius: "var(--radius)", zIndex: 0 }}
transition={{ type: "spring", bounce: 0.2, duration: 0.4 }}
/>
)}
<span className={styles.tabLabel}>{tab.label}</span>
</TabsTrigger>
))}
</TabsList>
<motion.div
animate={{ height: bounds.height }}
className={styles.contentContainer}
initial={{ height: "auto" }}
transition={{ type: "spring", bounce: 0.2, duration: 0.4 }}
>
<div className={styles.contentWrapper} ref={ref}>
<AnimatePresence
custom={direction}
initial={false}
mode="popLayout"
>
{activeTabData && (
<motion.div
animate="active"
className={styles.tabContent}
custom={direction}
exit="exit"
initial="initial"
key={activeTab}
style={{ padding: "1.5rem" }}
transition={contentTransition}
variants={contentVariants}
>
<div>
<h3 className={styles.heading}>{activeTabData.heading}</h3>
<p className={styles.description}>
{activeTabData.description}
</p>
</div>
<div className={styles.buttonGroup}>
{activeTabData.buttons.map((button) => (
<Button
key={button.label}
size="sm"
variant={button.variant}
>
{button.label}
</Button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
</Tabs>
</div>
);
}
<Tabs>
<TabsList>
<TabsTrigger />
</TabsList>
<TabsContent />
</Tabs>