Docs
DirectionAwareTabs
DirectionAwareTabs
Tabs that animate direction correctly
Installation
Copy and paste the following code into your project.
"use client"
import { ReactNode, useMemo, useState } from "react"
import { AnimatePresence, MotionConfig, motion } from "framer-motion"
import useMeasure from "react-use-measure"
import { cn } from "@/lib/utils"
type Tab = {
id: number
label: string
content: ReactNode
}
interface OgImageSectionProps {
tabs: Tab[]
className?: string
rounded?: string
onChange?: () => void
}
function DirectionAwareTabs({
tabs,
className,
rounded,
onChange,
}: OgImageSectionProps) {
const [activeTab, setActiveTab] = useState(0)
const [direction, setDirection] = useState(0)
const [isAnimating, setIsAnimating] = useState(false)
const [ref, bounds] = useMeasure()
const content = useMemo(() => {
const activeTabContent = tabs.find((tab) => tab.id === activeTab)?.content
return activeTabContent || null
}, [activeTab, tabs])
const handleTabClick = (newTabId: number) => {
if (newTabId !== activeTab && !isAnimating) {
const newDirection = newTabId > activeTab ? 1 : -1
setDirection(newDirection)
setActiveTab(newTabId)
onChange ? onChange() : null
}
}
const variants = {
initial: (direction: number) => ({
x: 300 * direction,
opacity: 0,
filter: "blur(4px)",
}),
active: {
x: 0,
opacity: 1,
filter: "blur(0px)",
},
exit: (direction: number) => ({
x: -300 * direction,
opacity: 0,
filter: "blur(4px)",
}),
}
return (
<div className=" flex flex-col items-center w-full">
<div
className={cn(
"flex space-x-1 border border-none rounded-full cursor-pointer bg-neutral-600 px-[3px] py-[3.2px] shadow-inner-shadow",
className,
rounded
)}
>
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => handleTabClick(tab.id)}
className={cn(
"relative rounded-full px-3.5 py-1.5 text-xs sm:text-sm font-medium text-neutral-200 transition focus-visible:outline-1 focus-visible:ring-1 focus-visible:outline-none flex gap-2 items-center ",
activeTab === tab.id
? "text-white"
: "hover:text-neutral-300/60 text-neutral-200/80",
rounded
)}
style={{ WebkitTapHighlightColor: "transparent" }}
>
{activeTab === tab.id && (
<motion.span
layoutId="bubble"
className="absolute inset-0 z-10 bg-neutral-700 mix-blend-difference shadow-inner-shadow border border-white/10"
style={rounded ? { borderRadius: 9 } : { borderRadius: 9999 }}
transition={{ type: "spring", bounce: 0.19, duration: 0.4 }}
/>
)}
{tab.label}
</button>
))}
</div>
<MotionConfig transition={{ duration: 0.4, type: "spring", bounce: 0.2 }}>
<motion.div
className="relative mx-auto w-full h-full overflow-hidden"
initial={false}
animate={{ height: bounds.height }}
>
<div className="p-1" ref={ref}>
<AnimatePresence
custom={direction}
mode="popLayout"
onExitComplete={() => setIsAnimating(false)}
>
<motion.div
key={activeTab}
variants={variants}
initial="initial"
animate="active"
exit="exit"
custom={direction}
onAnimationStart={() => setIsAnimating(true)}
onAnimationComplete={() => setIsAnimating(false)}
>
{content}
</motion.div>
</AnimatePresence>
</div>
</motion.div>
</MotionConfig>
</div>
)
}
export { DirectionAwareTabs }
Update the import paths to match your project setup.
Usage
import { DirectionAwareTabs } from "@/components/ui/direction-aware-tabs"
const DirectionAwareTabsDemo = ({}) => {
const tabs = [
{
id: 0,
label: "ocean",
content: (
<div className="border border-black/10 w-full flex flex-col items-center p-4 rounded-lg gap-3">
<BgAnimateButton animation="spin-fast" gradient="ocean">
Button
</BgAnimateButton>
<BgAnimateButton animation="spin" gradient="ocean">
Button
</BgAnimateButton>
<BgAnimateButton animation="spin-slow" gradient="ocean">
Button
</BgAnimateButton>
</div>
),
},
{
id: 1,
label: "forest",
content: (
<div className="border border-black/10 w-full flex flex-col items-center p-4 rounded-lg gap-3">
<BgAnimateButton animation="spin-fast" gradient="forest">
Button
</BgAnimateButton>
<BgAnimateButton animation="spin" gradient="forest">
Button
</BgAnimateButton>
<BgAnimateButton animation="spin-slow" gradient="forest">
Button
</BgAnimateButton>
</div>
),
},
{
id: 2,
label: "default",
content: (
<div className="border border-black/10 w-full flex flex-col items-center gap-3 p-4">
<BgAnimateButton animation="spin-fast" gradient="default">
Button
</BgAnimateButton>
<BgAnimateButton animation="spin" gradient="default">
Button
</BgAnimateButton>
<BgAnimateButton animation="spin-slow" gradient="default">
Button
</BgAnimateButton>
</div>
),
},
{
id: 3,
label: "sunset",
content: (
<div className="border border-black/10 w-full flex flex-col items-center p-4 rounded-lg gap-3">
<BgAnimateButton animation="spin-fast" gradient="sunset">
Button
</BgAnimateButton>
<BgAnimateButton animation="spin" gradient="sunset">
Button
</BgAnimateButton>
<BgAnimateButton animation="spin-slow" gradient="sunset">
Button
</BgAnimateButton>
</div>
),
},
]
return (
<div className="">
<DirectionAwareTabs tabs={tabs} />
</div>
)
}
export default DirectionAwareTabsDemo