Dock

A dock component that displays a list of items.

Dock - Basic

Loading...

Usage Guide

Install the clsx and tailwind-merge packages:

npm install clsx tw-merge framer-motion

Next, add cn utility:

import { twMerge } from "tailwind-merge";
import { clsx, type ClassValue } from "clsx";

export function cn(...inputs: ClassValue[]) {
    return twMerge(clsx(inputs));
}

Copy and paste the following component source.

"use client";

import { cn } from "@/lib/utils";
import React, { useState } from "react";
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";

export interface DockItemProps {
    icon: React.ReactNode;
    label: string;
    onClick?: () => void;
    className?: string;
}

interface DockProps {
    children: React.ReactNode;
    className?: string;
}

// Dock Component
export function Dock({ children, className }: DockProps) {
    return (
        <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            transition={{ type: "spring", stiffness: 260, damping: 20 }}
            className={cn(
                "flex px-4 py-2 gap-4 border rounded-2xl shadow-md bg-neutral-100 dark:bg-neutral-800",
                className
            )}
        >
            {children}
        </motion.div>
    );
}

// DockItem Component
export function DockItem({ icon, label, onClick, className, ...props }: DockItemProps) {
    const [isHovered, setIsHovered] = useState(false);

    const x = useMotionValue(0);
    const springConfig = { stiffness: 200, damping: 10 };
    const rotate = useSpring(useTransform(x, [-50, 50], [-25, 25]), springConfig);
    const translateX = useSpring(useTransform(x, [-50, 50], [-10, 10]), springConfig);

    const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
        const rect = event.currentTarget.getBoundingClientRect();
        const centerX = rect.left + rect.width / 2;
        x.set(event.clientX - centerX);
    };

    const handleMouseLeave = () => {
        x.set(0);
        setIsHovered(false);
    };

    return (
        <motion.div
            onMouseEnter={() => setIsHovered(true)}
            onMouseLeave={handleMouseLeave}
            onMouseMove={handleMouseMove}
            whileHover={{
                scale: 1.2,
                transition: { type: "spring", stiffness: 300, damping: 20 }
            }}
            whileTap={{ scale: 0.9 }}
            className={cn(
                "relative flex items-center justify-center w-12 h-12 rounded-full shadow-md cursor-pointer border bg-neutral-100 dark:bg-neutral-900 hover:shadow-lg",
                className
            )}
            onClick={onClick}
            {...props}
        >
            {icon}
            {isHovered && (
                <motion.div
                    initial={{ opacity: 0, y: 10, scale: 0.8 }}
                    animate={{ opacity: 1, y: 0, scale: 1 }}
                    exit={{ opacity: 0, y: 10, scale: 0.8 }}
                    style={{
                        rotate, // Dynamic tilt
                        translateX, // Subtle side-to-side movement
                        transformOrigin: "center center"
                    }}
                    className="absolute bottom-full mb-2 px-2 py-1 bg-black/75 text-white dark:bg-white dark:text-black text-xs rounded shadow-lg"
                >
                    {label}
                </motion.div>
            )}
        </motion.div>
    );
}

Props - Dock

PropTypeDescriptionDefault
childrenReactNodeThe content to be rendered inside the component_
classNamestringCustom class to apply to the component._

Props - DockItem

PropTypeDescriptionDefault
iconReactNodeIcon component_
labelstringText to show on tooltip_
classNamestringCustom class to apply to the component._