Back to all components
Animated Menu
Animated menu with smooth transitions and content switching
1

Installation

Use the following command to install the component:

npx shadcn@latest add https://ui.sanjid.shop/r/menu.json
pnpm dlx shadcn@latest add https://ui.sanjid.shop/r/menu.json
yarn shadcn@latest add https://ui.sanjid.shop/r/menu.json
bunx --bun shadcn@latest add https://ui.sanjid.shop/r/menu.json

Install the following dependencies:

npm install motion
pnpm add motion
yarn add motion
bun add motion

Add util file

lib/utils.ts
1import { ClassValue, clsx } from "clsx";2import { twMerge } from "tailwind-merge";3
4export function cn(...inputs: ClassValue[]) {5  return twMerge(clsx(inputs));6}

Copy and paste the following code into your project.

components/ui/menu.tsx
1"use client";2
3import React, { useState } from "react";4import { motion, AnimatePresence } from "motion/react";5
6export type TabItem = {7  id: string | number;8  label: string;9  icon?: React.ReactNode;10  content: React.ReactNode;11};12
13export type AnimatedTabsProps = {14  tabs: TabItem[];15  defaultActiveId?: string | number;16  layout?: "horizontal" | "grid"; // More control over tab layout17  className?: string;18};19
20export function AnimatedTabs({21  tabs,22  defaultActiveId,23  layout = "grid",24  className = "",25}: AnimatedTabsProps) {26  const [activeId, setActiveId] = useState(defaultActiveId ?? tabs[0]?.id);27
28  const activeTab = tabs.find((t) => t.id === activeId);29
30  return (31    <div32      className={`items-left flex flex-col gap-8 rounded-xl bg-gray-100 p-4 dark:bg-neutral-900 ${className}`}33    >34      {/* --- Tabs Header --- */}35      <motion.div36        className={`w-fit rounded-xl bg-white p-2 dark:bg-black ${layout === "grid"37            ? "grid grid-cols-2 gap-2 sm:grid-cols-4"38            : "flex flex-wrap justify-center gap-2"39          }`}40        initial={{ opacity: 0, y: 20 }}41        animate={{ opacity: 1, y: 0 }}42      >43        {tabs.map((tab) => {44          const isActive = tab.id === activeId;45          return (46            <motion.button47              key={tab.id}48              onClick={() => setActiveId(tab.id)}49              className={`group relative rounded-md px-2 py-1 text-sm transition-colors ${isActive50                  ? "text-primary font-semibold"51                  : "text-muted-foreground hover:text-primary"52                }`}53              whileHover={{ scale: 1.05 }}54              whileTap={{ scale: 0.95 }}55              aria-pressed={isActive}56            >57              {isActive && (58                <motion.div59                  layoutId="activeTab"60                  className="border-primary/10 bg-primary/10 absolute inset-0 rounded-lg border backdrop-blur-xs"61                  transition={{ type: "spring", bounce: 0.25, duration: 0.5 }}62                />63              )}64              <span className="relative flex items-center gap-1.5">65                {tab.icon && (66                  <span className="flex h-5 w-5 items-center justify-center">67                    {tab.icon}68                  </span>69                )}70                {tab.label}71              </span>72            </motion.button>73          );74        })}75      </motion.div>76
77      {/* --- Tab Content --- */}78      <motion.div79        className="border-primary/10 bg-primary/5 w-full max-w-md rounded-lg border p-6 backdrop-blur-lg"80        initial={{ opacity: 0, scale: 0.9 }}81        animate={{ opacity: 1, scale: 1 }}82        transition={{ delay: 0.2 }}83      >84        <AnimatePresence mode="wait">85          <motion.div86            key={activeId}87            initial={{ opacity: 0, x: 30 }}88            animate={{ opacity: 1, x: 0 }}89            exit={{ opacity: 0, x: -30 }}90            transition={{ duration: 0.25 }}91            className="space-y-2"92          >93            <h3 className="text-xl font-semibold">{activeTab?.label}</h3>94            <div className="text-muted-foreground text-sm">95              {activeTab?.content}96            </div>97          </motion.div>98        </AnimatePresence>99      </motion.div>100    </div>101  );102}

Update the import paths to match your project setup.

Basic Usage

1import { AnimatedMenu } from "@/components/snippets/menu/Demo";2
3const tabs = [4  {5    id: 1,6    label: "Home",7    icon: <IconHome className="h-4 w-4" />,8    content: <div>Home</div>,9  },10  {11    id: 2,12    label: "Projects",13    icon: <IconDeviceLaptop className="h-4 w-4" />,14    content: <div>Projects</div>,15  },16  {17    id: 3,18    label: "About",19    icon: <IconUser className="h-4 w-4" />,20    content: <div>About</div>,21  },22  {23    id: 4,24    label: "Contact",25    icon: <IconMail className="h-4 w-4" />,26    content: <div>Contact</div>,27  },28];29
30<AnimatedMenu tabs={tabs} />;

Props

PropTypeRequiredDefaultDescription
classNamestringNoThe className of the tab to display
idnumberNoThe id of the tab to display
labelstringNoThe label of the tab to display
iconReact.ReactNodeNoThe icon of the tab to display
contentReact.ReactNodeNoThe content of the tab to display

Examples

1<AnimatedMenu tabs={tabs} />

1<AnimatedMenu2  tabs={tabs}3  className="bg-linear-to-r from-purple-500 to-pink-500"4/>

Best Practices

  1. Clear Labels: Use descriptive and concise menu labels
  2. Consistent Icons: Use consistent icon styles throughout
  3. Logical Grouping: Group related menu items together
  4. Keyboard Navigation: Ensure all items are accessible via keyboard
  5. Loading States: Show loading indicators for dynamic content

Performance Tips

  1. Lazy Loading: Load content only when menu items are selected
  2. Animation Optimization: Use will-change CSS property for better performance
  3. Memoization: Memoize menu items to prevent unnecessary re-renders
  4. Debouncing: Debounce rapid menu changes to improve performance