Installation
Install dependencies
npm i motion clsx tailwind-merge @tabler/icons-react
Add util file
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Copy the source code
components/ui/floating-dock.tsx
/**
* Note: Use position fixed according to your needs
* Desktop navbar is better positioned at the bottom
* Mobile navbar is better positioned at bottom right.
**/
import { cn } from "@/lib/utils";
import { IconLayoutNavbarCollapse } from "@tabler/icons-react";
import {
AnimatePresence,
MotionValue,
motion,
useMotionValue,
useSpring,
useTransform,
} from "motion/react";
import Link from "next/link";
import { useRef, useState } from "react";
export const FloatingDock = ({
items,
desktopClassName,
mobileClassName,
}: {
items: { title: string; icon: React.ReactNode; href: string }[];
desktopClassName?: string;
mobileClassName?: string;
}) => {
return (
<>
<FloatingDockDesktop items={items} className={desktopClassName} />
<FloatingDockMobile items={items} className={mobileClassName} />
</>
);
};
const FloatingDockMobile = ({
items,
className,
}: {
items: { title: string; icon: React.ReactNode; href: string }[];
className?: string;
}) => {
const [open, setOpen] = useState(false);
return (
<div className={cn("relative block md:hidden", className)}>
<AnimatePresence>
{open && (
<motion.div
layoutId="nav"
className="absolute bottom-full mb-2 inset-x-0 flex flex-col gap-2"
>
{items.map((item, idx) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 10 }}
animate={{
opacity: 1,
y: 0,
}}
exit={{
opacity: 0,
y: 10,
transition: {
delay: idx * 0.05,
},
}}
transition={{ delay: (items.length - 1 - idx) * 0.05 }}
>
<Link
href={item.href}
key={item.title}
className="h-10 w-10 rounded-full bg-gray-50 dark:bg-neutral-900 flex items-center justify-center"
>
<div className="h-4 w-4">{item.icon}</div>
</Link>
</motion.div>
))}
</motion.div>
)}
</AnimatePresence>
<button
onClick={() => setOpen(!open)}
className="h-10 w-10 rounded-full bg-gray-50 dark:bg-neutral-800 flex items-center justify-center"
>
<IconLayoutNavbarCollapse className="h-5 w-5 text-neutral-500 dark:text-neutral-400" />
</button>
</div>
);
};
const FloatingDockDesktop = ({
items,
className,
}: {
items: { title: string; icon: React.ReactNode; href: string }[];
className?: string;
}) => {
let mouseX = useMotionValue(Infinity);
return (
<motion.div
onMouseMove={(e) => mouseX.set(e.pageX)}
onMouseLeave={() => mouseX.set(Infinity)}
className={cn(
"mx-auto hidden md:flex h-16 gap-4 items-end rounded-2xl bg-gray-50 dark:bg-neutral-900 px-4 pb-3",
className
)}
>
{items.map((item) => (
<IconContainer mouseX={mouseX} key={item.title} {...item} />
))}
</motion.div>
);
};
function IconContainer({
mouseX,
title,
icon,
href,
}: {
mouseX: MotionValue;
title: string;
icon: React.ReactNode;
href: string;
}) {
let ref = useRef<HTMLDivElement>(null);
let distance = useTransform(mouseX, (val) => {
let bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };
return val - bounds.x - bounds.width / 2;
});
let widthTransform = useTransform(distance, [-150, 0, 150], [40, 80, 40]);
let heightTransform = useTransform(distance, [-150, 0, 150], [40, 80, 40]);
let widthTransformIcon = useTransform(distance, [-150, 0, 150], [20, 40, 20]);
let heightTransformIcon = useTransform(
distance,
[-150, 0, 150],
[20, 40, 20]
);
let width = useSpring(widthTransform, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
let height = useSpring(heightTransform, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
let widthIcon = useSpring(widthTransformIcon, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
let heightIcon = useSpring(heightTransformIcon, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
const [hovered, setHovered] = useState(false);
return (
<Link href={href}>
<motion.div
ref={ref}
style={{ width, height }}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className="aspect-square rounded-full bg-gray-200 dark:bg-neutral-800 flex items-center justify-center relative"
>
<AnimatePresence>
{hovered && (
<motion.div
initial={{ opacity: 0, y: 10, x: "-50%" }}
animate={{ opacity: 1, y: 0, x: "-50%" }}
exit={{ opacity: 0, y: 2, x: "-50%" }}
className="px-2 py-0.5 whitespace-pre rounded-md bg-gray-100 border dark:bg-neutral-800 dark:border-neutral-900 dark:text-white border-gray-200 text-neutral-700 absolute left-1/2 -translate-x-1/2 -top-8 w-fit text-xs"
>
{title}
</motion.div>
)}
</AnimatePresence>
<motion.div
style={{ width: widthIcon, height: heightIcon }}
className="flex items-center justify-center"
>
{icon}
</motion.div>
</motion.div>
</Link>
);
}
Props
Floating Dock
Prop Name | Type | Description |
---|---|---|
items | { title: string; icon: React.ReactNode; href: string }[] | Array of items to display in the dock. |
desktopClassName | string | Optional class name for the desktop dock. |
mobileClassName | string | Optional class name for the mobile dock. |
Floating Dock Mobile
Prop Name | Type | Description |
---|---|---|
items | { title: string; icon: React.ReactNode; href: string }[] | Array of items to display in the mobile dock. |
className | string | Optional class name for the mobile dock. |
Floating Dock Desktop
Prop Name | Type | Description |
---|---|---|
items | { title: string; icon: React.ReactNode; href: string }[] | Array of items to display in the desktop dock. |
className | string | Optional class name for the desktop dock. |
IconContainer Component
Prop Name | Type | Description |
---|---|---|
mouseX | MotionValue | Motion value for the mouse X position. |
title | string | Title of the item. |
icon | React.ReactNode | Icon to display for the item. |
href | string | URL to navigate to when the item is clicked. |
This component is inspired by floating dock on menu on Rauno's website and it's implementation on Build UI.
Build websites faster and 10x better than your competitors with Aceternity UI Pro
With the best in class components and templates, stand out from the crowd and get more attention to your website. Trusted by founders and entrepreneurs from all over the world.
I've been working with Manu for a couple of months now and I can't express enough how impressed I am with his talent. Manu's JavaScript/React web UI programming skills are through the roof. He's he...
Tony Pujals
Founder at Fantastic Realms, Tech Lead at Google