DocumentationDock
Glass Dock
A macOS-inspired glassmorphic dock with scale magnification effects on hover.
Dock
Install using CLI
npx shadcn@latest add "https://vengeance-ui.vercel.app/r/glass-dock.json"Install Manually
1
Install dependencies
npm install framer-motion clsx tailwind-merge
2
Add util file
lib/utils.ts
import { ClassValue, clsx } from "clsx";import { twMerge } from "tailwind-merge";export function cn(...inputs: ClassValue[]) {return twMerge(clsx(inputs));}
3
Add styles to global CSS
Add the following styles to your app/globals.css file
/* Glass Component Borders */.glass-border {border: 1px solid rgba(0, 0, 0, 0.15);}.dark .glass-border {border: 1px solid rgba(255, 255, 255, 0.2);}
4
Copy the source code
Copy the code below and paste it into components/ui/glass-dock.tsx
'use client';import React, { useState } from 'react';import { LucideIcon } from 'lucide-react';import { motion, AnimatePresence } from 'framer-motion';import { cn } from '@/lib/utils';export interface DockItem {title: string;icon: LucideIcon;onClick?: () => void;href?: string;}export interface GlassDockProps extends React.HTMLAttributes<HTMLDivElement> {items: DockItem[];dockClassName?: string;}export const GlassDock = React.forwardRef<HTMLDivElement, GlassDockProps>(({items,className,dockClassName,...props},ref) => {const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);const [direction, setDirection] = useState(0);const [isDark, setIsDark] = useState(false);React.useEffect(() => {const checkTheme = () => {setIsDark(document.documentElement.classList.contains('dark'));};checkTheme();const observer = new MutationObserver(checkTheme);observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });return () => observer.disconnect();}, []);const handleMouseEnter = (index: number) => {if (hoveredIndex !== null && index !== hoveredIndex) {setDirection(index > hoveredIndex ? 1 : -1);}setHoveredIndex(index);};const getTooltipPosition = (index: number) => index * 52 + 12;return (<divref={ref}className={cn('w-max', className)}{...props}><divclassName={cn("relative flex gap-4 items-center px-6 py-4 rounded-2xl","glass-border bg-white/80 dark:bg-black/80","backdrop-blur-xl shadow-2xl",dockClassName)}onMouseLeave={() => {setHoveredIndex(null);setDirection(0);}}><AnimatePresence>{hoveredIndex !== null && (<motion.divlayoutinitial={{ opacity: 0, scale: 0.92, y: 12 }}animate={{opacity: 1,scale: 1,y: -60,x: getTooltipPosition(hoveredIndex),}}exit={{ opacity: 0, scale: 0.92, y: 12 }}transition={{ type: 'spring', stiffness: 120, damping: 18 }}className="absolute top-0 left-0 pointer-events-none z-30"><divclassName={cn('px-5 py-2 rounded-lg','bg-black text-white dark:bg-white dark:text-black','shadow-md flex items-center justify-center','border border-neutral-700 dark:border-neutral-300','min-w-[100px] ')}><div className="relative h-4 flex items-center justify-center overflow-hidden w-full"><AnimatePresence mode="popLayout" custom={direction}><motion.spankey={items[hoveredIndex].title}custom={direction}initial={{x: direction > 0 ? 35 : -35,opacity: 0,filter: 'blur(6px)',}}animate={{x: 0,opacity: 1,filter: 'blur(0px)',}}exit={{x: direction > 0 ? -35 : 35,opacity: 0,filter: 'blur(6px)',}}transition={{duration: 0.3,ease: 'easeOut',}}className="text-[13px] font-medium tracking-wide whitespace-nowrap">{items[hoveredIndex].title}</motion.span></AnimatePresence></div></div></motion.div>)}</AnimatePresence>{items.map((el, index) => {const Icon = el.icon;const isHovered = hoveredIndex === index;const handleClick = () => {if (el.onClick) {el.onClick();} else if (el.href) {window.location.href = el.href;}};return (<divkey={el.title}onMouseEnter={() => handleMouseEnter(index)}onClick={handleClick}className="relative w-10 h-10 flex items-center justify-center cursor-pointer"role="button"tabIndex={0}onKeyDown={(e) => {if (e.key === 'Enter' || e.key === ' ') {handleClick();}}}><motion.divwhileTap={{ scale: 0.95 }}animate={{scale: isHovered ? 1.1 : 1,y: isHovered ? -3 : 0,}}transition={{ type: 'spring', stiffness: 300, damping: 24 }}><Iconsize={22}strokeWidth={2}className={cn('transition-colors duration-200',isHovered? 'text-neutral-900 dark:text-white': 'text-neutral-500 dark:text-neutral-400')}/></motion.div></div>);})}</div></div>);});GlassDock.displayName = 'GlassDock';export default GlassDock;
Usage
1import { GlassDock } from "@/components/ui/glass-dock"2import {3Home,4Terminal,5Layout,6Archive,7History,8Twitter,9Github,10} from "lucide-react"1112export function GlassDockDemo() {13const items = [14 { title: 'Home', icon: Home, href: '#' },15 { title: 'Products', icon: Terminal, href: '#' },16 { title: 'Components', icon: Layout, href: '#' },17 { title: 'Archive', icon: Archive, href: '#' },18 { title: 'Changelog', icon: History, href: '#' },19 { title: 'Twitter', icon: Twitter, href: '#' },20 { title: 'Github', icon: Github, href: '#' },21];2223return (24 <div className="flex items-center justify-center h-[300px] w-full">25 <GlassDock items={items} />26 </div>27);28}
Props
| Prop Name | Type | Default | Description |
|---|---|---|---|
| items | array | - | Array of items to display in the dock. |
| className | string | - | Class for the outer wrapper. |
| dockClassName | string | - | Class for the inner dock container. |