DocsDropdown Menu
Dropdown Menu
Vercel Like Animated Dropdown Menu
Installation
Install Dependencies
npm i clsx tailwind-merge framer-motion
Create @/utils/cn.ts file
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Dropdown component
'use client'
import React, { useState, createContext, useContext } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { cn } from '@/utils/cn'
const DirectionContext = createContext<{
direction: 'rtl' | 'ltr' | null
setAnimationDirection: (tab: number | null) => void
} | null>(null)
const CurrentTabContext = createContext<{
currentTab: number | null
} | null>(null)
export const Dropdown: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [currentTab, setCurrentTab] = useState<null | number>(null)
const [direction, setDirection] = useState<'rtl' | 'ltr' | null>(null)
const setAnimationDirection = (tab: number | null) => {
if (typeof currentTab === 'number' && typeof tab === 'number') {
setDirection(currentTab > tab ? 'rtl' : 'ltr')
} else if (tab === null) {
setDirection(null)
}
setCurrentTab(tab)
}
return (
<DirectionContext.Provider value={{ direction, setAnimationDirection }}>
<CurrentTabContext.Provider value={{ currentTab }}>
<span
onMouseLeave={() => setAnimationDirection(null)}
className={'relative flex h-fit gap-2'}>
{children}
</span>
</CurrentTabContext.Provider>
</DirectionContext.Provider>
)
}
export const TriggerWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { currentTab } = useContext(CurrentTabContext)!
const { setAnimationDirection } = useContext(DirectionContext)!
return (
<>
{React.Children.map(children, (e, i) => (
<button
onMouseEnter={() => setAnimationDirection(i + 1)}
onClick={() => setAnimationDirection(i + 1)}
className={`flex h-10 items-center gap-0.5 rounded-md px-4 py-2 text-sm font-medium text-neutral-950 transition-colors dark:text-white ${
currentTab === i + 1 ? 'bg-neutral-100 dark:bg-neutral-800 [&>svg]:rotate-180' : ''
}`}>
{e}
</button>
))}
</>
)
}
export const Trigger: React.FC<{ children: React.ReactNode; className?: string }> = ({
children,
className
}) => {
return (
<>
<span className={cn('', className)}>{children}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="relative top-[1px] ml-1 h-3 w-3 transition-transform duration-200 "
aria-hidden="true">
<path d="m6 9 6 6 6-6" />
</svg>
</>
)
}
export const Tabs: React.FC<{ children: React.ReactNode; className?: string }> = ({
children,
className
}) => {
const { currentTab } = useContext(CurrentTabContext)!
const { direction } = useContext(DirectionContext)!
return (
<>
<motion.div
id="overlay-content"
initial={{
opacity: 0,
scale: 0.98
}}
animate={
currentTab
? {
opacity: 1,
scale: 1
}
: { opacity: 0, scale: 0.98 }
}
className="absolute left-0 top-[calc(100%_+_6px)] w-auto">
<div className="absolute -top-[6px] left-0 right-0 h-[6px]" />
<div
className={cn(
'rounded-md border border-neutral-200 backdrop-blur-xl transition-all duration-300 dark:border-neutral-800',
className
)}>
{React.Children.map(children, (e, i) => (
<div className="overflow-hidden">
<AnimatePresence>
{currentTab !== null && (
<motion.div exit={{ opacity: 0 }}>
{currentTab === i + 1 && (
<motion.div
initial={{
opacity: 0,
x: direction === 'ltr' ? 100 : direction === 'rtl' ? -100 : 0
}}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.2 }}>
{e}
</motion.div>
)}
</motion.div>
)}
</AnimatePresence>
</div>
))}
</div>
</motion.div>
</>
)
}
export const Tab: React.FC<{ children: React.ReactNode; className?: string }> = ({
children,
className
}) => {
return <div className={cn('h-full w-[500px]', className)}>{children}</div>
}