All components
Direction Aware Tabs
navigationTab component with direction-aware animations and smooth transitions
responsive · 360px
Install
Same command in any shadcn project — React (Vite/CRA), Next.js, Remix, Astro, and more:
$
npx shadcn@latest add https://your-domain/r/direction-aware-tabs.jsonUsage
"use client";
import { DirectionAwareTabs } from "@/registry/cult-ui/direction-aware-tabs";
const tabs = [
{
id: 0,
label: "Design",
content: (
<div className="p-6 text-center text-white">
<h3 className="text-lg font-semibold mb-2">Design System</h3>
<p className="text-gray-400 text-sm">
Build beautiful interfaces with our comprehensive design system. Tokens,
components, and patterns all in one place.
</p>
</div>
),
},
{
id: 1,
label: "Develop",
content: (
<div className="p-6 text-center text-white">
<h3 className="text-lg font-semibold mb-2">Developer Tools</h3>
<p className="text-gray-400 text-sm">
Powerful APIs and SDKs to build faster. Type-safe, well-documented, and
ready for production use.
</p>
</div>
),
},
{
id: 2,
label: "Deploy",
content: (
<div className="p-6 text-center text-white">
<h3 className="text-lg font-semibold mb-2">Deployment Pipeline</h3>
<p className="text-gray-400 text-sm">
Ship with confidence using our zero-config deployment infrastructure.
Preview, stage, and produce at any scale.
</p>
</div>
),
},
];
export default function Demo() {
return (
<div className="w-full max-w-md mx-auto p-6 bg-neutral-900 rounded-2xl">
<DirectionAwareTabs tabs={tabs} />
</div>
);
}Component source
"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 }Dependencies
motionreact-use-measure
Source: Cult UI