Docs
Animated Dock
A sleek dock-style navigation bar, inspired by macOS, that combines glassmorphic design with functionality. With smooth animations and responsive icons, it enhances navigation for a modern web application.
Installation
Install dependencies
npm install framer-motion lucide-react
Run the following command
It will create a new file animated-dock.tsx
inside the components/animata/container
directory.
mkdir -p components/animata/container && touch components/animata/container/animated-dock.tsx
Paste the code
Open the newly created file and paste the following code:
import { cn } from "@/lib/utils"; // Import utility for conditional class names
import {
AnimatePresence, // Enables animation presence detection
MotionValue, // Type for motion values
motion, // Main component for animations
useMotionValue, // Hook to create a motion value
useSpring, // Hook to create smooth spring animations
useTransform, // Hook to transform motion values
} from "framer-motion";
import Link from "next/link"; // Next.js Link component for navigation
import React, { useRef, useState } from "react"; // Importing React hooks
import { Menu, X } from "lucide-react"; // Importing icons from lucide-react
// Interface for props accepted by the AnimatedDock component
interface AnimatedDockProps {
items: { title: string; icon: React.ReactNode; href: string }[]; // Array of menu items
largeClassName?: string; // Optional class name for large dock
smallClassName?: string; // Optional class name for small dock
}
// Main AnimatedDock component that renders both LargeDock and SmallDock
export default function AnimatedDock({ items, largeClassName, smallClassName }: AnimatedDockProps) {
return (
<>
{/* Render LargeDock for larger screens */}
<LargeDock items={items} className={largeClassName} />
{/* Render SmallDock for smaller screens */}
<SmallDock items={items} className={smallClassName} />
</>
);
}
// Component for the large dock, visible on larger screens
const LargeDock = ({
items,
className,
}: {
items: { title: string; icon: React.ReactNode; href: string }[]; // Items to display
className?: string; // Optional class name
}) => {
const mouseXPosition = useMotionValue(Infinity); // Create a motion value for mouse X position
return (
<motion.div
onMouseMove={(e) => mouseXPosition.set(e.pageX)} // Update mouse X position on mouse move
onMouseLeave={() => mouseXPosition.set(Infinity)} // Reset on mouse leave
className={cn(
"mx-auto hidden h-16 items-end gap-4 rounded-2xl bg-white/10 px-4 pb-3 dark:bg-black/10 md:flex", // Large dock styles
className,
"border border-gray-200/30 backdrop-blur-sm dark:border-gray-800/30",
)}
>
{/* Render each dock icon */}
{items.map((item) => (
<DockIcon mouseX={mouseXPosition} key={item.title} {...item} />
))}
</motion.div>
);
};
// Component for individual icons in the dock
function DockIcon({
mouseX,
title,
icon,
href,
}: {
mouseX: MotionValue; // Motion value for mouse position
title: string; // Title of the icon
icon: React.ReactNode; // Icon component
href: string; // Link destination
}) {
const ref = useRef<HTMLDivElement>(null); // Ref for measuring distance from mouse
// Calculate the distance from the mouse to the icon
const distanceFromMouse = useTransform(mouseX, (val) => {
const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 }; // Get icon bounds
return val - bounds.x - bounds.width / 2; // Calculate distance from center
});
// Transform properties for width and height based on mouse distance
const widthTransform = useTransform(distanceFromMouse, [-150, 0, 150], [40, 80, 40]);
const heightTransform = useTransform(distanceFromMouse, [-150, 0, 150], [40, 80, 40]);
// Transform properties for icon size based on mouse distance
const iconWidthTransform = useTransform(distanceFromMouse, [-150, 0, 150], [20, 40, 20]);
const iconHeightTransform = useTransform(distanceFromMouse, [-150, 0, 150], [20, 40, 20]);
// Spring animations for smooth transitions
const width = useSpring(widthTransform, { mass: 0.1, stiffness: 150, damping: 12 });
const height = useSpring(heightTransform, { mass: 0.1, stiffness: 150, damping: 12 });
const iconWidth = useSpring(iconWidthTransform, { mass: 0.1, stiffness: 150, damping: 12 });
const iconHeight = useSpring(iconHeightTransform, { mass: 0.1, stiffness: 150, damping: 12 });
const [isHovered, setIsHovered] = useState(false); // State for hover effect
return (
<Link href={href}>
<motion.div
ref={ref} // Reference for measuring
style={{ width, height }} // Set dynamic width and height
onMouseEnter={() => setIsHovered(true)} // Handle mouse enter
onMouseLeave={() => setIsHovered(false)} // Handle mouse leave
className="relative flex aspect-square items-center justify-center rounded-full bg-white/20 text-black shadow-lg backdrop-blur-md dark:bg-black/20 dark:text-white"
>
<AnimatePresence>
{/* Tooltip that appears on hover */}
{isHovered && (
<motion.div
initial={{ opacity: 0, y: 10, x: "-50%" }} // Initial animation state
animate={{ opacity: 1, y: 0, x: "-50%" }} // Animation to visible state
exit={{ opacity: 0, y: 2, x: "-50%" }} // Animation to exit state
className="absolute -top-8 left-1/2 w-fit -translate-x-1/2 whitespace-pre rounded-md border border-gray-200 bg-white/80 px-2 py-0.5 text-xs text-neutral-700 dark:border-neutral-900 dark:bg-neutral-800 dark:text-white"
>
{title} {/* Tooltip text */}
</motion.div>
)}
</AnimatePresence>
<motion.div
style={{ width: iconWidth, height: iconHeight }} // Set dynamic icon size
className="flex items-center justify-center"
>
{icon} {/* Render the icon */}
</motion.div>
</motion.div>
</Link>
);
}
// Component for the small dock, visible on smaller screens
const SmallDock = ({
items,
className,
}: {
items: { title: string; icon: React.ReactNode; href: string }[]; // Items to display
className?: string; // Optional class name
}) => {
const [isOpen, setIsOpen] = useState(false); // State to manage open/close of the small dock
return (
<div className={cn("relative block md:hidden", className)}>
<AnimatePresence>
{/* Render menu items when open */}
{isOpen && (
<motion.div
layoutId="nav"
className="absolute inset-x-0 bottom-full mb-2 flex flex-col gap-2"
>
{items.map((item, index) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 10 }} // Initial animation state
animate={{ opacity: 1, y: 0 }} // Animation to visible state
exit={{
opacity: 0,
y: 10,
transition: { delay: index * 0.05 }, // Delay based on index
}}
transition={{ delay: (items.length - 1 - index) * 0.05 }} // Delay for exit animations
>
<Link
href={item.href}
key={item.title}
className="flex h-10 w-10 items-center justify-center rounded-full bg-white/20 text-black shadow-md backdrop-blur-md dark:bg-black/20 dark:text-white"
>
<div className="h-4 w-4">{item.icon}</div> {/* Render the icon */}
</Link>
</motion.div>
))}
</motion.div>
)}
</AnimatePresence>
{/* Button to toggle the small dock open/close */}
<button
onClick={() => setIsOpen(!isOpen)} // Toggle isOpen state on click
className="flex h-10 w-10 items-center justify-center rounded-full bg-white/20 text-black shadow-md backdrop-blur-md dark:bg-black/20 dark:text-white"
>
{/* Render the appropriate icon based on open/close state */}
{isOpen ? (
<X className="h-5 w-5" /> // Show close icon when open
) : (
<Menu className="h-5 w-5" /> // Show menu icon when closed
)}
</button>
</div>
);
};
Credits
Built by Eshan Singh with the help of Framer Motion. Inspired by Build UI