Animated Tabs
Animated tabs using Framer Motion with a smoothly transitioning active background that animates from one tab to another
Animated tabs using Framer Motion with a smoothly transitioning active background that animates from one tab to another
npm install framer-motion
1// @ts-nocheck2'use client';34import { cn } from '@/lib/utils';5import { AnimatePresence, motion } from 'framer-motion';6import React, {7ReactNode,8createContext,9useContext,10useEffect,11useState,12isValidElement,13} from 'react';14interface TabContextType {15activeTab: string;16setActiveTab: (value: string) => void;17wobbly: boolean;18hover: boolean;19defaultValue: string;20prevIndex: number;21setPrevIndex: (value: number) => void;22tabsOrder: string[];23}24const TabContext = createContext<TabContextType | undefined>(undefined);2526export const useTabs = () => {27const context = useContext(TabContext);28if (!context) {29throw new Error('useTabs must be used within a TabsProvider');30}31return context;32};3334interface TabsProviderProps {35children: ReactNode;36defaultValue: string;37wobbly?: boolean;38hover?: boolean;39}4041export const TabsProvider = ({42children,43defaultValue,44wobbly = true,45hover = false,46}: TabsProviderProps) => {47const [activeTab, setActiveTab] = useState(defaultValue);48const [prevIndex, setPrevIndex] = useState(0);49const [tabsOrder, setTabsOrder] = useState<string[]>([]);50useEffect(() => {51const order: string[] = [];52children?.map((child) => {53if (isValidElement(child)) {54if (child.type === TabsContent) {55order.push(child.props.value);56}57}58});59setTabsOrder(order);60}, [children]);6162return (63<TabContext.Provider64value={{65activeTab,66setActiveTab,67wobbly,68hover,69defaultValue,70setPrevIndex,71prevIndex,72tabsOrder,73}}74>75{children}76</TabContext.Provider>77);78};7980export const TabsBtn = ({ children, className, value }: any) => {81const {82activeTab,83setPrevIndex,84setActiveTab,85defaultValue,86hover,87wobbly,88tabsOrder,89} = useTabs();9091const handleClick = () => {92setPrevIndex(tabsOrder.indexOf(activeTab));93setActiveTab(value);94};9596return (97<>98<>99<motion.div100className={cn(101`cursor-pointer sm:p-2 p-1 sm:px-4 px-2 rounded-md relative `,102className103)}104onFocus={() => {105hover && handleClick();106}}107onMouseEnter={() => {108hover && handleClick();109}}110onClick={handleClick}111>112{children}113114{activeTab === value && (115<AnimatePresence mode='wait'>116<motion.div117transition={{118layout: {119duration: 0.2,120ease: 'easeInOut',121delay: 0.2,122},123}}124layoutId={defaultValue}125className='absolute w-full h-full left-0 top-0 dark:bg-base-dark bg-white rounded-md z-[1]'126/>127</AnimatePresence>128)}129130{wobbly ? (131<>132{activeTab === value && (133<AnimatePresence mode='wait'>134<motion.div135transition={{136layout: {137duration: 0.4,138ease: 'easeInOut',139delay: 0.04,140},141}}142layoutId={defaultValue}143className='absolute w-full h-full left-0 top-0 dark:bg-base-dark bg-white rounded-md z-[1] tab-shadow'144/>145</AnimatePresence>146)}147{activeTab === value && (148<AnimatePresence mode='wait'>149<motion.div150transition={{151layout: {152duration: 0.4,153ease: 'easeOut',154delay: 0.2,155},156}}157layoutId={`${defaultValue}b`}158className='absolute w-full h-full left-0 top-0 dark:bg-base-dark bg-white rounded-md z-[1] tab-shadow'159/>160</AnimatePresence>161)}162</>163) : (164<></>165)}166</motion.div>167</>168</>169);170};171172export const TabsContent = ({ children, className, value, yValue }: any) => {173const { activeTab, tabsOrder, prevIndex } = useTabs();174const isForward = tabsOrder.indexOf(activeTab) > prevIndex;175return (176<>177<AnimatePresence mode='popLayout'>178{activeTab === value && (179<motion.div180initial={{ opacity: 0, y: yValue ? (isForward ? 10 : -10) : 0 }}181animate={{ opacity: 1, y: 0 }}182exit={{ opacity: 0, y: yValue ? (isForward ? -50 : 50) : 0 }}183transition={{184duration: 0.3,185ease: 'easeInOut',186delay: 0.5,187}}188className={cn(' p-2 px-4 rounded-md relative', className)}189>190{activeTab === value ? children : null}191</motion.div>192)}193</AnimatePresence>194</>195);196};
1<TabsProvider defaultValue={'login'} wobbly={true}>2<div className='flex justify-center mt-2'>3<div className='flex items-center w-fit dark:bg-[#1a1c20] bg-gray-200 p-1 dark:text-white text-black rounded-md border'>4<TabsBtn value='login'>5<span className='relative z-[2] uppercase'>Login</span>6</TabsBtn>7<TabsBtn value='register'>8<span className='relative z-[2] uppercase'>Register</span>9</TabsBtn>10</div>11</div>1213<TabsContent value='login'>14<div className='p-2 border'></div>15</TabsContent>16<TabsContent value='register'>17<div className='p-2 border'></div>18</TabsContent>19</TabsProvider>
1import { useEffect, useState } from 'react';23export function useMediaQuery(query: string) {4const [value, setValue] = useState(false);56useEffect(() => {7function onChange(event: MediaQueryListEvent) {8setValue(event.matches);9}1011const result = matchMedia(query);12result.addEventListener('change', onChange);13setValue(result.matches);1415return () => result.removeEventListener('change', onChange);16}, [query]);1718return value;19}
Prop | Type | Default | Description |
---|---|---|---|
children | ReactNode | The content to be rendered inside the TabsProvider. | |
defaultValue | string | The default value for the selected tab. | |
wobbly | boolean | true | Whether the tabs should have a wobbly effect. |
hover | boolean | false | Whether to enable hover effects on the tabs. |
Please Enter Your Details to Sign In
Don't have an account yet?