Animated Tabs
Animated Tabs is a React and Tailwind CSS navigation component from UI Layouts, licensed MIT. Copy it and paste it into your project.
UI Layouts
MIT
navigation
What it is
Animated Tabs is a React and Tailwind CSS navigation component from UI Layouts, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i motion
Licence
This component comes from UI Layouts and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Animated Tabs
'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';
'use client';
import { TabsBtn, TabsContent, TabsProvider } from '@/components/ui/tab';
import { AnimatePresence, motion } from 'motion/react';
import Image from 'next/image';
import React, { useState } from 'react';
const effectArr = [
{
id: 1,
name: 'basic',
},
{
id: 2,
name: 'hover',
},
{
id: 3,
name: 'wobbly',
},
];
function PreviewTab() {
const [checkEffect, setCheckEffect] = useState('basic');
return (
<>
<div className='flex bg-black dark:bg-neutral-900 w-fit ml-auto mb-4 gap-1 p-1 rounded-md text-white'>
{effectArr?.map((effect, index) => {
return (
<>
<motion.button
onClick={() => setCheckEffect(effect?.name)}
className={`py-1 px-3 rounded-md capitalize relative `}
>
<span className='z-10 relative'>{effect?.name}</span>
{checkEffect === effect.name && (
<motion.div
transition={{
layout: {
duration: 0.2,
ease: 'easeInOut',
},
}}
layoutId={'magnetic'}
className='absolute w-full h-full left-0 top-0 bg-[conic-gradient(from_90deg_at_50%_50%,#8494ff_0%,#3749be_50%,#7d8efc_100%)] rounded-md z-1 tab-shadow'
/>
)}
</motion.button>
</>
);
})}
</div>
<div className='border bg-white/10 dark:bg-black/40 backdrop-blur-xs rounded-md p-4 relative'>
<TabsProvider
defaultValue={'design'}
wobbly={checkEffect === 'wobbly' ? true : false}
hover={checkEffect === 'hover' ? true : false}
>
<div className='flex justify-center mt-2'>
<div className='flex items-center w-fit dark:bg-[#1d2025] bg-neutral-200 p-1 dark:text-white text-black rounded-md border'>
<TabsBtn value='design'>
<span className='relative z-2 uppercase sm:text-base text-xs'>design</span>
</TabsBtn>
<TabsBtn value='collaborate'>
<span className='relative z-2 uppercase sm:text-base text-xs'>collaborate</span>
</TabsBtn>
<TabsBtn value='share'>
<span className='relative z-2 uppercase sm:text-base text-xs'>share</span>
</TabsBtn>
<TabsBtn value='publish'>
<span className='relative z-2 uppercase sm:text-base text-xs'>publish</span>
</TabsBtn>
</div>
</div>
<TabsContent value='design'>
<div className='w-full'>
<Image
src={
'https://images.unsplash.com/photo-1506097425191-7ad538b29cef?q=80&w=1000&auto=format&fit=crop'
}
width={1000}
height={1000}
alt='preview_img'
className='w-[850px] object-cover h-full mx-auto rounded-md'
/>
</div>
</TabsContent>
<TabsContent value='collaborate'>
<div className='w-full'>
<Image
src={
'https://images.unsplash.com/photo-1557804506-669a67965ba0?q=80&w=1000&auto=format&fit=crop'
}
width={1000}
height={1000}
alt='preview_img'
className='w-[850px] object-cover h-full mx-auto rounded-md'
/>
</div>
</TabsContent>
<TabsContent value='share'>
<div className='w-full'>
<Image
src={
'https://images.unsplash.com/photo-1665470909901-162912ec16f7?q=80&w=1000&auto=format&fit=crop'
}
width={1000}
height={1000}
alt='preview_img'
className='w-[850px] object-cover h-full mx-auto rounded-md'
/>
</div>
</TabsContent>
<TabsContent value='publish'>
<div className='w-full'>
<Image
src={
'https://images.unsplash.com/photo-1694022861804-840f61d1c452?q=80&w=1000&auto=format&fit=crop'
}
width={1000}
height={1000}
alt='preview_img'
className='w-[850px] object-cover h-full mx-auto rounded-md'
/>
</div>
</TabsContent>
</TabsProvider>
</div>
</>
);
}
export default PreviewTab;