Direction Aware Tabs
Direction Aware Tabs is a React and Tailwind CSS navigation component from Cult UI, licensed MIT. Copy it and paste it into your project.
Cult UI
MIT
navigation
What it is
Direction Aware Tabs is a React and Tailwind CSS navigation component from Cult UI, 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 react-use-measure
Licence
This component comes from Cult UI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Direction Aware Tabs
"use client"
import { ReactNode, useMemo, useState } from "react"
import { AnimatePresence, motion, MotionConfig } from "motion/react"
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
/** Outer container radius (e.g. `rounded-lg`) */
rounded?: string
/** Inner tab/bubble radius — should be outer radius minus container padding (~3px) */
roundedInner?: string
onChange?: () => void
}
function DirectionAwareTabs({
tabs,
className,
rounded,
roundedInner,
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 ? roundedInner : undefined
)}
style={{ WebkitTapHighlightColor: "transparent" }}
>
{activeTab === tab.id && (
<motion.span
layoutId="bubble"
className={cn(
"absolute inset-0 z-10 bg-neutral-700 mix-blend-difference shadow-inner-shadow border border-white/10",
rounded ? roundedInner : "rounded-full"
)}
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 }