// app/components/Tabs/Tab.tsx
"use client";
import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
const tabs = [
{ id: "tab1", label: "Overview" },
{ id: "tab2", label: "Settings" },
];
const content = {
tab1: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Praesentium, animi.",
tab2: "Dolor sit amet consectetur adipisicing elit. Quasi, voluptatum.",
};
function Tab() {
const [activeTab, setActiveTab] = useState("tab1");
return (
<div className="w-full max-w-md">
{/* Tab buttons */}
<div className="relative flex p-1 bg-gray-200 rounded-xl">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`relative z-10 flex-1 text-sm font-medium px-4 py-2 rounded-lg cursor-pointer transition-colors duration-200 ${
activeTab === tab.id
? "text-white"
: "text-gray-500 hover:text-gray-700"
}`}
>
{activeTab === tab.id && (
<motion.div
layoutId="activeTab"
className="absolute inset-0 bg-black rounded-lg"
transition={{ type: "spring", stiffness: 400, damping: 30 }}
/>
)}
<span className="relative z-10">{tab.label}</span>
</button>
))}
</div>
{/* Tab content */}
<div className="mt-6 min-h-20">
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={activeTab}
initial={{ opacity: 0, y: 8, filter: "blur(4px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
exit={{ opacity: 0, y: -8, filter: "blur(4px)" }}
transition={{ duration: 0.2, ease: "easeOut" }}
>
<p className="text-gray-700 leading-relaxed">
{content[activeTab as keyof typeof content]}
</p>
</motion.div>
</AnimatePresence>
</div>
</div>
);
}
export default Tab;Component details
Overview, install command, dependencies, and project links.
A smooth animated tab switcher built with React, Tailwind CSS, and Framer Motion. The active tab indicator slides between buttons using Framer Motion's layoutId - so the black pill animates fluidly from one tab to the next with a spring transition instead of an abrupt jump. Tab content fades in and out with a vertical slide and blur filter using AnimatePresence with mode="wait" - the exit animation completes before the new content enters, keeping transitions clean and never overlapping. Tabs and content are defined as plain arrays and objects at the top of the file - adding a new tab is just adding one entry to each. Written with TypeScript, fully typed content access using keyof typeof content. Works as a "use client" component in Next.js App Router or as a standard React component in Vite.
Install
npm install framer-motionDependencies
Discussion
Feedback, implementation tips, and usage notes.
Comments
Sign in to join the conversation
Similar Components
More from the same category


