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