Pill Tabs
An animated pill-style tab component built with ShadCN, Framer Motion, and Tailwind CSS.
Preview
Installation
Install dependencies
# Install Shadcn Button components
npx shadcn@latest add button
# Install Framer Motion for animations
npm install framer-motion
Create the component file
mkdir -p components/shsfui/tabs && touch components/shsfui/switch/pill-tabs.tsx
Add the component code
Open the newly created file and paste the following code:
"use client";
import * as React from "react";
import { motion } from "framer-motion";
import { cn } from "@/utils/cn";
type TabItemType = {
id: string;
label: string;
};
type PillTabsProps = {
tabs?: TabItemType[];
defaultActiveId?: string;
onTabChange?: (id: string) => void;
className?: string;
};
const MOCK_TABS: TabItemType[] = [
{ id: "home", label: "Home" },
{ id: "about", label: "About" },
{ id: "contact", label: "Contact" },
];
const PillTabs = React.forwardRef<HTMLDivElement, PillTabsProps>(
(props, ref) => {
const {
tabs = MOCK_TABS,
defaultActiveId = tabs[0]?.id,
onTabChange,
className,
} = props;
const [activeTab, setActiveTab] = React.useState(defaultActiveId);
const handleClick = React.useCallback(
(id: string) => {
setActiveTab(id);
onTabChange?.(id);
},
[onTabChange]
);
return (
<div
ref={ref}
className={cn(
"flex items-center gap-1 p-1 bg-background rounded-full border",
className
)}
>
{tabs.map((tab) => (
<button
key={tab.id}
type="button"
onClick={() => handleClick(tab.id)}
className={cn(
"relative px-4 py-2 rounded-full transition touch-none",
"text-sm font-medium",
activeTab === tab.id
? "text-primary-foreground"
: "text-muted-foreground hover:text-foreground"
)}
>
{activeTab === tab.id && (
<motion.div
layoutId="pill-tabs-active-pill"
className="absolute inset-0 bg-primary rounded-full"
transition={{ type: "spring", duration: 0.5 }}
/>
)}
<span className="relative z-10">{tab.label}</span>
</button>
))}
</div>
);
}
);
PillTabs.displayName = "PillTabs";
export PillTabs;