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 next
pnpm install framer-motion @tabler/icons-react react next
yarn add framer-motion @tabler/icons-react react next
bun add framer-motion @tabler/icons-react react next
npx install framer-motion @tabler/icons-react react next

Create 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>
  );
}

Copy the Demo

"use client";import { FloatingDock } from "@/components/ui/floating-dock";import {  IconBrandGithub,  IconBrandX,  IconExchange,  IconHome,  IconNewSection,  IconTerminal2,} from "@tabler/icons-react";import Image from "next/image";const iconClass = "h-full w-full text-muted-foreground transition-colors group-hover:text-primary";const links = [  {    title: "Home",    icon: <IconHome className={iconClass} />,    href: "#",  },  {    title: "Products",    icon: <IconTerminal2 className={iconClass} />,    href: "#",  },  {    title: "Components",    icon: <IconNewSection className={iconClass} />,    href: "#",  },  {    title: "Craft UI",    icon: (      <Image        src="https://assets.aceternity.com/logo-dark.png"        width={20}        height={20}        alt="Craft UI logo"        className="rounded-full"      />    ),    href: "#",  },  {    title: "Changelog",    icon: <IconExchange className={iconClass} />,    href: "#",  },  {    title: "Twitter",    icon: <IconBrandX className={iconClass} />,    href: "#",  },  {    title: "GitHub",    icon: <IconBrandGithub className={iconClass} />,    href: "#",  },];export default function FloatingDockDemo() {  return (    <div className="flex h-[35rem] w-full items-center justify-center">      <FloatingDock        mobileClassName="translate-y-20" // only for demo, remove for production        items={links}      />    </div>  );}

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