Home/Components/Buttons/Animated Tab Component React Framer Motion
Code · Live Preview
Componenttsx
// 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;
DesktopSandpack

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

terminal
npm
npm install framer-motion

Dependencies

Framer Motion
Added Aug 17, 2026

Discussion

Feedback, implementation tips, and usage notes.

0 threads

Comments

Sign in to join the conversation