Animated Tabs

Animated tabs using Framer Motion with a smoothly transitioning active background that animates from one tab to another

design
collaborate
share
publish
preview_img

Installation

npm install framer-motion
tab.tsx
1
// @ts-nocheck
2
'use client';
3
4
import { cn } from '@/lib/utils';
5
import { AnimatePresence, motion } from 'framer-motion';
6
import React, {
7
ReactNode,
8
createContext,
9
useContext,
10
useEffect,
11
useState,
12
isValidElement,
13
} from 'react';
14
interface TabContextType {
15
activeTab: string;
16
setActiveTab: (value: string) => void;
17
wobbly: boolean;
18
hover: boolean;
19
defaultValue: string;
20
prevIndex: number;
21
setPrevIndex: (value: number) => void;
22
tabsOrder: string[];
23
}
24
const TabContext = createContext<TabContextType | undefined>(undefined);
25
26
export const useTabs = () => {
27
const context = useContext(TabContext);
28
if (!context) {
29
throw new Error('useTabs must be used within a TabsProvider');
30
}
31
return context;
32
};
33
34
interface TabsProviderProps {
35
children: ReactNode;
36
defaultValue: string;
37
wobbly?: boolean;
38
hover?: boolean;
39
}
40
41
export const TabsProvider = ({
42
children,
43
defaultValue,
44
wobbly = true,
45
hover = false,
46
}: TabsProviderProps) => {
47
const [activeTab, setActiveTab] = useState(defaultValue);
48
const [prevIndex, setPrevIndex] = useState(0);
49
const [tabsOrder, setTabsOrder] = useState<string[]>([]);
50
useEffect(() => {
51
const order: string[] = [];
52
children?.map((child) => {
53
if (isValidElement(child)) {
54
if (child.type === TabsContent) {
55
order.push(child.props.value);
56
}
57
}
58
});
59
setTabsOrder(order);
60
}, [children]);
61
62
return (
63
<TabContext.Provider
64
value={{
65
activeTab,
66
setActiveTab,
67
wobbly,
68
hover,
69
defaultValue,
70
setPrevIndex,
71
prevIndex,
72
tabsOrder,
73
}}
74
>
75
{children}
76
</TabContext.Provider>
77
);
78
};
79
80
export const TabsBtn = ({ children, className, value }: any) => {
81
const {
82
activeTab,
83
setPrevIndex,
84
setActiveTab,
85
defaultValue,
86
hover,
87
wobbly,
88
tabsOrder,
89
} = useTabs();
90
91
const handleClick = () => {
92
setPrevIndex(tabsOrder.indexOf(activeTab));
93
setActiveTab(value);
94
};
95
96
return (
97
<>
98
<>
99
<motion.div
100
className={cn(
101
`cursor-pointer sm:p-2 p-1 sm:px-4 px-2 rounded-md relative `,
102
className
103
)}
104
onFocus={() => {
105
hover && handleClick();
106
}}
107
onMouseEnter={() => {
108
hover && handleClick();
109
}}
110
onClick={handleClick}
111
>
112
{children}
113
114
{activeTab === value && (
115
<AnimatePresence mode='wait'>
116
<motion.div
117
transition={{
118
layout: {
119
duration: 0.2,
120
ease: 'easeInOut',
121
delay: 0.2,
122
},
123
}}
124
layoutId={defaultValue}
125
className='absolute w-full h-full left-0 top-0 dark:bg-base-dark bg-white rounded-md z-[1]'
126
/>
127
</AnimatePresence>
128
)}
129
130
{wobbly ? (
131
<>
132
{activeTab === value && (
133
<AnimatePresence mode='wait'>
134
<motion.div
135
transition={{
136
layout: {
137
duration: 0.4,
138
ease: 'easeInOut',
139
delay: 0.04,
140
},
141
}}
142
layoutId={defaultValue}
143
className='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.div
150
transition={{
151
layout: {
152
duration: 0.4,
153
ease: 'easeOut',
154
delay: 0.2,
155
},
156
}}
157
layoutId={`${defaultValue}b`}
158
className='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
};
171
172
export const TabsContent = ({ children, className, value, yValue }: any) => {
173
const { activeTab, tabsOrder, prevIndex } = useTabs();
174
const isForward = tabsOrder.indexOf(activeTab) > prevIndex;
175
return (
176
<>
177
<AnimatePresence mode='popLayout'>
178
{activeTab === value && (
179
<motion.div
180
initial={{ opacity: 0, y: yValue ? (isForward ? 10 : -10) : 0 }}
181
animate={{ opacity: 1, y: 0 }}
182
exit={{ opacity: 0, y: yValue ? (isForward ? -50 : 50) : 0 }}
183
transition={{
184
duration: 0.3,
185
ease: 'easeInOut',
186
delay: 0.5,
187
}}
188
className={cn(' p-2 px-4 rounded-md relative', className)}
189
>
190
{activeTab === value ? children : null}
191
</motion.div>
192
)}
193
</AnimatePresence>
194
</>
195
);
196
};
base
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>
12
13
<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>
useMediaQuery.tsx
1
import { useEffect, useState } from 'react';
2
3
export function useMediaQuery(query: string) {
4
const [value, setValue] = useState(false);
5
6
useEffect(() => {
7
function onChange(event: MediaQueryListEvent) {
8
setValue(event.matches);
9
}
10
11
const result = matchMedia(query);
12
result.addEventListener('change', onChange);
13
setValue(result.matches);
14
15
return () => result.removeEventListener('change', onChange);
16
}, [query]);
17
18
return value;
19
}

Props

PropTypeDefaultDescription
childrenReactNodeThe content to be rendered inside the TabsProvider.
defaultValuestringThe default value for the selected tab.
wobblybooleantrueWhether the tabs should have a wobbly effect.
hoverbooleanfalseWhether to enable hover effects on the tabs.

Example

Creative Tab

Accordion
Globe
Mouse Trail
gallery

Register Tab

Login
Register
img

Welcome Back

Please Enter Your Details to Sign In

OR

Don't have an account yet?