All components
Animated Tabs
navigationUi-Layouts component.
responsive · 688px
Install
Same command in any shadcn project — React (Vite/CRA), Next.js, Remix, Astro, and more:
$
npx shadcn@latest add https://your-domain/r/animated-tabs.jsonUsage
"use client";
import { TabsProvider, TabsBtn, TabsContent } from "@/registry/ui-layouts/tab";
const TABS = [
{
value: "overview",
label: "Overview",
content:
"An overview gives stakeholders a concise picture of scope, goals, and current status. Keep it scannable — bullet-points and short sentences outperform dense paragraphs every time.",
},
{
value: "features",
label: "Features",
content:
"Features are the building blocks of your product. Prioritise ruthlessly: ship the 20 % that delivers 80 % of value first, then iterate based on real usage data.",
},
{
value: "pricing",
label: "Pricing",
content:
"Simple, transparent pricing builds trust. Three tiers — free, pro, and enterprise — cover most SaaS businesses without overwhelming prospective customers with choice.",
},
];
export default function Demo() {
return (
<div className="w-full h-full flex flex-col items-center justify-center p-8 gap-6">
<TabsProvider defaultValue="overview" wobbly>
{/* Tab bar */}
<div className="flex gap-1 p-1 rounded-lg bg-muted w-fit">
{TABS.map((tab) => (
<TabsBtn key={tab.value} value={tab.value}>
<span className="relative z-10 text-sm font-medium px-1 select-none">
{tab.label}
</span>
</TabsBtn>
))}
</div>
{/* Tab panels */}
<div className="relative w-full max-w-lg min-h-[120px]">
{TABS.map((tab) => (
<TabsContent key={tab.value} value={tab.value} yValue>
<p className="text-sm text-muted-foreground leading-relaxed">{tab.content}</p>
</TabsContent>
))}
</div>
</TabsProvider>
</div>
);
}Component source
'use client';
import { cn } from '@/lib/utils';
import { AnimatePresence, motion } from 'motion/react';
import React, {
createContext,
isValidElement,
type ReactNode,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
// Improved TypeScript interfaces with more specific types
interface TabContextType {
activeTab: string;
setActiveTab: (value: string) => void;
wobbly: boolean;
hover: boolean;
defaultValue: string;
prevIndex: number;
setPrevIndex: (value: number) => void;
tabsOrder: string[];
}
const TabContext = createContext<TabContextType | undefined>(undefined);
// Custom hook with memoization
export const useTabs = () => {
const context = useContext(TabContext);
if (!context) {
throw new Error('useTabs must be used within a TabsProvider');
}
return context;
};
// Props interfaces with more specific types
interface TabsProviderProps {
children: ReactNode;
defaultValue: string;
wobbly?: boolean;
hover?: boolean;
}
interface TabsBtnProps {
children: ReactNode;
className?: string;
value: string;
}
interface TabsContentProps {
children: ReactNode;
className?: string;
value: string;
yValue?: boolean;
}
export const TabsProvider: React.FC<TabsProviderProps> = React.memo(
({ children, defaultValue, wobbly = true, hover = false }) => {
// Use useCallback to memoize state setters
const [activeTab, setActiveTab] = useState(defaultValue);
const [prevIndex, setPrevIndex] = useState(0);
// Memoize tabs order to prevent unnecessary recalculations
const tabsOrder = useMemo(() => {
return React.Children.toArray(children)
.filter((child) => isValidElement(child) && child.type === TabsContent)
.map((child) => (child as React.ReactElement<any>).props.value);
}, [children]);
// Memoize context value to prevent unnecessary re-renders
const contextValue = useMemo(
() => ({
activeTab,
setActiveTab,
wobbly,
hover,
defaultValue,
setPrevIndex,
prevIndex,
tabsOrder,
}),
[activeTab, setActiveTab, wobbly, hover, defaultValue, prevIndex, tabsOrder]
);
return <TabContext.Provider value={contextValue}>{children}</TabContext.Provider>;
}
);
// Memoized TabsBtn component
export const TabsBtn: React.FC<TabsBtnProps> = React.memo(({ children, className, value }) => {
const { activeTab, setPrevIndex, setActiveTab, defaultValue, hover, wobbly, tabsOrder } =
useTabs();
// Use useCallback to memoize the click handler
const handleClick = useCallback(() => {
setPrevIndex(tabsOrder.indexOf(activeTab));
setActiveTab(value);
}, [setPrevIndex, tabsOrder, activeTab, setActiveTab, value]);
return (
<motion.div
className={cn(`cursor-pointer 2xl:p-2 p-2 2xl:px-4 px-2 rounded-md relative`, className)}
onFocus={() => hover && handleClick()}
onMouseEnter={() => hover && handleClick()}
onClick={handleClick}
>
{children}
<AnimatePresence mode='wait'>
{activeTab === value && (
<>
<motion.div
transition={{
layout: {
duration: 0.2,
ease: 'easeInOut',
delay: 0.2,
},
}}
layoutId={defaultValue}
className='absolute w-full h-full left-0 top-0 dark:bg-primary-base bg-white rounded-md z-1'
/>
{wobbly && (
<>
<motion.div
transition={{
layout: {
duration: 0.4,
ease: 'easeInOut',
delay: 0.04,
},
}}
layoutId={defaultValue}
className='absolute w-full h-full left-0 top-0 dark:bg-primary-base bg-white rounded-md z-1 tab-shadow'
/>
<motion.div
transition={{
layout: {
duration: 0.4,
ease: 'easeOut',
delay: 0.2,
},
}}
layoutId={`${defaultValue}b`}
className='absolute w-full h-full left-0 top-0 dark:bg-primary-base bg-white rounded-md z-1 tab-shadow'
/>
</>
)}
</>
)}
</AnimatePresence>
</motion.div>
);
});
// Memoized TabsContent component
export const TabsContent: React.FC<TabsContentProps> = React.memo(
({ children, className, value, yValue }) => {
const { activeTab, tabsOrder, prevIndex } = useTabs();
// Memoize direction calculation
const isForward = useMemo(
() => tabsOrder.indexOf(activeTab) > prevIndex,
[tabsOrder, activeTab, prevIndex]
);
return (
<AnimatePresence mode='popLayout'>
{activeTab === value && (
<motion.div
initial={{ opacity: 0, y: yValue ? (isForward ? 10 : -10) : 0 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: yValue ? (isForward ? -50 : 50) : 0 }}
transition={{
duration: 0.3,
ease: 'easeInOut',
delay: 0.5,
}}
className={cn('p-2 px-4 rounded-md relative', className)}
>
{children}
</motion.div>
)}
</AnimatePresence>
);
}
);
// Add display names for better debugging
TabsProvider.displayName = 'TabsProvider';
TabsBtn.displayName = 'TabsBtn';
TabsContent.displayName = 'TabsContent';Dependencies
motion
Source: Ui-Layouts