Floating Dock
A floating dock navigation component with smooth hover animations. Features desktop and mobile variants with interactive icon scaling.
Installation
npm install framer-motion @tabler/icons-react react nextpnpm install framer-motion @tabler/icons-react react nextyarn add framer-motion @tabler/icons-react react nextbun add framer-motion @tabler/icons-react react nextnpx install framer-motion @tabler/icons-react react nextCreate the Floating Dock Component
Create components/ui/floating-dock.tsx:
"use client";
import { cn } from "@/lib/utils";
import { IconLayoutNavbarCollapse } from "@tabler/icons-react";
import {
AnimatePresence,
motion,
useMotionValue,
useSpring,
useTransform,
type MotionValue,
} from "framer-motion";
import type { ReactNode } from "react";
import { useRef, useState } from "react";
type DockItem = {
title: string;
icon: ReactNode;
href: string;
};
interface FloatingDockProps {
items: DockItem[];
desktopClassName?: string;
mobileClassName?: string;
}
export function FloatingDock({
items,
desktopClassName,
mobileClassName,
}: FloatingDockProps) {
return (
<>
<FloatingDockDesktop items={items} className={desktopClassName} />
<FloatingDockMobile items={items} className={mobileClassName} />
</>
);
}
function FloatingDockMobile({
items,
className,
}: {
items: DockItem[];
className?: string;
}) {
const [open, setOpen] = useState(false);
return (
<div className={cn("relative block md:hidden", className)}>
<AnimatePresence>
{open && (
<motion.div
layoutId="nav"
className="absolute inset-x-0 bottom-full mb-2 flex flex-col items-center 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 }}
>
<a
href={item.href}
className="flex h-10 w-10 items-center justify-center rounded-full border border-border bg-card text-muted-foreground shadow-theme-sm"
>
<div className="h-4 w-4">{item.icon}</div>
</a>
</motion.div>
))}
</motion.div>
)}
</AnimatePresence>
<button
type="button"
onClick={() => setOpen(!open)}
aria-label="Toggle dock"
className="flex h-10 w-10 items-center justify-center rounded-full border border-border bg-card text-muted-foreground shadow-theme-md"
>
<IconLayoutNavbarCollapse className="h-5 w-5" />
</button>
</div>
);
}
function FloatingDockDesktop({
items,
className,
}: {
items: DockItem[];
className?: string;
}) {
const mouseX = useMotionValue(Infinity);
return (
<motion.div
onMouseMove={(event) => mouseX.set(event.pageX)}
onMouseLeave={() => mouseX.set(Infinity)}
className={cn(
"mx-auto hidden h-16 items-end gap-4 rounded-2xl border border-border bg-card px-4 pb-3 shadow-theme-md md:flex",
className,
)}
>
{items.map((item) => (
<IconContainer mouseX={mouseX} key={item.title} {...item} />
))}
</motion.div>
);
}
function IconContainer({
mouseX,
title,
icon,
href,
}: {
mouseX: MotionValue<number>;
title: string;
icon: ReactNode;
href: string;
}) {
const ref = useRef<HTMLDivElement>(null);
const [hovered, setHovered] = useState(false);
const distance = useTransform(mouseX, (val) => {
const bounds = ref.current?.getBoundingClientRect() ?? {
x: 0,
width: 0,
};
return val - bounds.x - bounds.width / 2;
});
const widthTransform = useTransform(
distance,
[-150, 0, 150],
[40, 80, 40],
);
const heightTransform = useTransform(
distance,
[-150, 0, 150],
[40, 80, 40],
);
const widthTransformIcon = useTransform(
distance,
[-150, 0, 150],
[20, 40, 20],
);
const heightTransformIcon = useTransform(
distance,
[-150, 0, 150],
[20, 40, 20],
);
const width = useSpring(widthTransform, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
const height = useSpring(heightTransform, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
const widthIcon = useSpring(widthTransformIcon, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
const heightIcon = useSpring(heightTransformIcon, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
return (
<a href={href}>
<motion.div
ref={ref}
style={{ width, height }}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className="group relative flex aspect-square items-center justify-center rounded-full bg-secondary text-muted-foreground transition-colors hover:text-foreground"
>
<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="absolute -top-8 left-1/2 w-fit whitespace-pre rounded-md border border-border bg-card px-2 py-0.5 text-xs text-foreground shadow-theme-sm"
>
{title}
</motion.div>
)}
</AnimatePresence>
<motion.div
style={{ width: widthIcon, height: heightIcon }}
className="flex items-center justify-center"
>
{icon}
</motion.div>
</motion.div>
</a>
);
}Usage
The dock icons rest in `text-muted-foreground` and shift to `text-primary` on hover — add `group-hover:text-primary` (with a `group` on the icon circle) or simply use `text-primary` for an always-branded look.import { FloatingDock } from "@/components/ui/floating-dock";
import {
IconHome,
IconBrandGithub,
IconBrandX,
} from "@tabler/icons-react";
const iconClass = "h-full w-full text-muted-foreground transition-colors group-hover:text-primary";
export default function MyComponent() {
const links = [
{
title: "Home",
icon: <IconHome className={iconClass} />,
href: "/",
},
{
title: "GitHub",
icon: <IconBrandGithub className={iconClass} />,
href: "https://github.com",
},
{
title: "Twitter",
icon: <IconBrandX className={iconClass} />,
href: "https://twitter.com",
},
];
return (
<div className="flex items-center justify-center h-screen">
<FloatingDock items={links} />
</div>
);
}Features
- 🎯 Interactive Icons: Icons scale smoothly on hover with spring animations
- 📱 Responsive: Separate desktop and mobile layouts
- 🎨 Smooth Animations: Powered by framer-motion for fluid transitions
- 🌙 Dark Mode: Seamlessly works with light and dark themes
- 🖱️ Hover Effects: Icons expand based on mouse position
- 📱 Mobile Menu: Collapsible pill menu for mobile devices
Positioning
The component supports custom positioning via className props:
<FloatingDock
desktopClassName="fixed bottom-4 left-1/2 -translate-x-1/2"
mobileClassName="fixed bottom-4 right-4"
items={links}
/>Note:
- Desktop navbar is better positioned at the bottom
- Mobile navbar is better positioned at bottom right
Customization
You can customize the dock by:
- Icons: Use any React node as icons (@tabler/icons-react, custom SVGs, images)
- Colors: Modify Tailwind classes for background and text colors
- Sizes: Adjust the dock height and icon sizes via className
- Animations: The spring animations are configurable in the component
Examples
Custom Icons with Images
import Image from "next/image";
const links = [
{
title: "Logo",
icon: (
<Image
src="/logo.png"
width={20}
height={20}
alt="Logo"
className="rounded-full"
/>
),
href: "/",
},
];Fixed Position Dock
<FloatingDock
desktopClassName="fixed bottom-8 left-1/2 -translate-x-1/2 z-50"
items={links}
/>Last updated on
