UI Components Medium
macOS Dock
A macOS-style dock with smooth icon magnification on hover. Icons scale up based on proximity to the cursor, creating a fluid fisheye effect.
Open in Lab
MCP
css javascript vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #0a0a0a;
color: #f1f5f9;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
padding: clamp(0.75rem, 3vw, 2rem);
}
.dock-scene {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 3rem;
padding-bottom: 1rem;
}
.dock-hint {
color: #64748b;
font-size: 0.875rem;
letter-spacing: 0.02em;
}
/* ── Dock bar ── */
.dock {
display: flex;
align-items: flex-end;
gap: clamp(0.125rem, 0.5vw, 0.25rem);
padding: clamp(0.375rem, 1.5vw, 0.625rem) clamp(0.5rem, 2vw, 0.875rem);
max-width: 100%;
overflow-x: auto;
overflow-y: visible;
-webkit-overflow-scrolling: touch;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 1.25rem;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
}
/* ── Dock item ── */
.dock-item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
transition: transform 0.15s ease;
}
/* ── Icon container ── */
.dock-icon {
width: clamp(36px, 10vw, 48px);
height: clamp(36px, 10vw, 48px);
display: grid;
place-items: center;
border-radius: 0.75rem;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.3), rgba(168, 85, 247, 0.3));
border: 1px solid rgba(255, 255, 255, 0.1);
color: #e2e8f0;
transition: width 0.2s ease, height 0.2s ease, background 0.2s ease;
}
.dock-icon svg {
width: 55%;
height: 55%;
}
.dock-item:hover .dock-icon {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.5), rgba(168, 85, 247, 0.5));
}
/* ── Tooltip ── */
.dock-tooltip {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%) translateY(4px);
background: rgba(15, 23, 42, 0.95);
color: #f1f5f9;
font-size: 0.75rem;
font-weight: 500;
padding: 0.3rem 0.6rem;
border-radius: 0.375rem;
border: 1px solid rgba(255, 255, 255, 0.1);
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease, transform 0.15s ease;
}
.dock-item:hover .dock-tooltip {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* ── Reduced motion ── */
@media (prefers-reduced-motion: reduce) {
.dock-item,
.dock-icon,
.dock-tooltip {
transition: none;
}
}(function () {
"use strict";
const dock = document.getElementById("dock");
if (!dock) return;
const items = dock.querySelectorAll(".dock-item");
const BASE_SIZE = 48;
const MAX_SIZE = 72;
const RANGE = 200; // px — how far the magnification reaches
function magnify(mouseX) {
items.forEach((item) => {
const icon = item.querySelector(".dock-icon");
const rect = item.getBoundingClientRect();
const itemCenterX = rect.left + rect.width / 2;
const distance = Math.abs(mouseX - itemCenterX);
// Gaussian-like falloff
const scale = Math.max(
BASE_SIZE,
MAX_SIZE - ((MAX_SIZE - BASE_SIZE) * Math.pow(distance, 2)) / Math.pow(RANGE, 2)
);
const size = Math.round(Math.min(MAX_SIZE, Math.max(BASE_SIZE, scale)));
icon.style.width = size + "px";
icon.style.height = size + "px";
});
}
function resetSizes() {
items.forEach((item) => {
const icon = item.querySelector(".dock-icon");
icon.style.width = BASE_SIZE + "px";
icon.style.height = BASE_SIZE + "px";
});
}
dock.addEventListener("mousemove", (e) => {
magnify(e.clientX);
});
dock.addEventListener("mouseleave", () => {
resetSizes();
});
// Initialize sizes
resetSizes();
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>macOS Dock</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="dock-scene">
<p class="dock-hint">Hover over the dock icons</p>
<nav class="dock" id="dock">
<div class="dock-item" data-label="Finder">
<div class="dock-tooltip">Finder</div>
<div class="dock-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="4"/><path d="M3 9h18"/><circle cx="7" cy="6" r="0.8" fill="currentColor"/><circle cx="10" cy="6" r="0.8" fill="currentColor"/></svg>
</div>
</div>
<div class="dock-item" data-label="Mail">
<div class="dock-tooltip">Mail</div>
<div class="dock-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="4" width="20" height="16" rx="3"/><path d="m2 7 10 6 10-6"/></svg>
</div>
</div>
<div class="dock-item" data-label="Music">
<div class="dock-tooltip">Music</div>
<div class="dock-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
</div>
</div>
<div class="dock-item" data-label="Photos">
<div class="dock-tooltip">Photos</div>
<div class="dock-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="3"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>
</div>
</div>
<div class="dock-item" data-label="Messages">
<div class="dock-tooltip">Messages</div>
<div class="dock-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</div>
</div>
<div class="dock-item" data-label="Calendar">
<div class="dock-tooltip">Calendar</div>
<div class="dock-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="18" height="18" rx="3"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>
</div>
</div>
<div class="dock-item" data-label="Notes">
<div class="dock-tooltip">Notes</div>
<div class="dock-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8M8 17h4"/></svg>
</div>
</div>
<div class="dock-item" data-label="Settings">
<div class="dock-tooltip">Settings</div>
<div class="dock-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
</div>
</div>
</nav>
</div>
<script src="script.js"></script>
</body>
</html>import { useRef, useState, useCallback } from "react";
interface DockItem {
icon: React.ReactNode;
label: string;
onClick?: () => void;
}
interface DockProps {
items?: DockItem[];
baseSize?: number;
maxSize?: number;
magnifyRange?: number;
}
const defaultItems: DockItem[] = [
{
label: "Finder",
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<rect x="3" y="3" width="18" height="18" rx="4" />
<path d="M3 9h18" />
</svg>
),
},
{
label: "Mail",
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<rect x="2" y="4" width="20" height="16" rx="3" />
<path d="m2 7 10 6 10-6" />
</svg>
),
},
{
label: "Music",
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<path d="M9 18V5l12-2v13" />
<circle cx="6" cy="18" r="3" />
<circle cx="18" cy="16" r="3" />
</svg>
),
},
{
label: "Photos",
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<rect x="3" y="3" width="18" height="18" rx="3" />
<circle cx="8.5" cy="8.5" r="1.5" />
<path d="m21 15-5-5L5 21" />
</svg>
),
},
{
label: "Messages",
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
),
},
{
label: "Calendar",
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<rect x="3" y="4" width="18" height="18" rx="3" />
<path d="M16 2v4M8 2v4M3 10h18" />
</svg>
),
},
{
label: "Settings",
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<circle cx="12" cy="12" r="3" />
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
</svg>
),
},
];
export default function Dock({
items = defaultItems,
baseSize = 48,
maxSize = 72,
magnifyRange = 200,
}: DockProps) {
const dockRef = useRef<HTMLDivElement>(null);
const [sizes, setSizes] = useState<number[]>(items.map(() => baseSize));
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const handleMouseMove = useCallback(
(e: React.MouseEvent) => {
const dock = dockRef.current;
if (!dock) return;
const children = Array.from(dock.children) as HTMLElement[];
const newSizes = children.map((child) => {
const rect = child.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const distance = Math.abs(e.clientX - centerX);
const scale = Math.max(
baseSize,
maxSize - ((maxSize - baseSize) * Math.pow(distance, 2)) / Math.pow(magnifyRange, 2)
);
return Math.round(Math.min(maxSize, Math.max(baseSize, scale)));
});
setSizes(newSizes);
},
[baseSize, maxSize, magnifyRange]
);
const handleMouseLeave = useCallback(() => {
setSizes(items.map(() => baseSize));
setHoveredIndex(null);
}, [items, baseSize]);
return (
<div
style={{
minHeight: "100vh",
background: "#0a0a0a",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "flex-end",
padding: "2rem",
paddingBottom: "2rem",
fontFamily: "system-ui, -apple-system, sans-serif",
}}
>
<p style={{ color: "#64748b", fontSize: "0.875rem", marginBottom: "3rem" }}>
Hover over the dock icons
</p>
<div
ref={dockRef}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
style={{
display: "flex",
alignItems: "flex-end",
gap: "0.25rem",
padding: "0.625rem 0.875rem",
background: "rgba(255,255,255,0.06)",
border: "1px solid rgba(255,255,255,0.1)",
borderRadius: "1.25rem",
backdropFilter: "blur(16px)",
WebkitBackdropFilter: "blur(16px)",
}}
>
{items.map((item, i) => (
<div
key={item.label}
style={{
position: "relative",
display: "flex",
flexDirection: "column",
alignItems: "center",
cursor: "pointer",
}}
onMouseEnter={() => setHoveredIndex(i)}
onMouseLeave={() => setHoveredIndex(null)}
onClick={item.onClick}
>
{hoveredIndex === i && (
<div
style={{
position: "absolute",
bottom: "calc(100% + 8px)",
left: "50%",
transform: "translateX(-50%)",
background: "rgba(15,23,42,0.95)",
color: "#f1f5f9",
fontSize: "0.75rem",
fontWeight: 500,
padding: "0.3rem 0.6rem",
borderRadius: "0.375rem",
border: "1px solid rgba(255,255,255,0.1)",
whiteSpace: "nowrap",
}}
>
{item.label}
</div>
)}
<div
style={{
width: sizes[i],
height: sizes[i],
display: "grid",
placeItems: "center",
borderRadius: "0.75rem",
background: "linear-gradient(135deg, rgba(99,102,241,0.3), rgba(168,85,247,0.3))",
border: "1px solid rgba(255,255,255,0.1)",
color: "#e2e8f0",
transition: "width 0.2s ease, height 0.2s ease",
}}
>
<div style={{ width: "55%", height: "55%" }}>{item.icon}</div>
</div>
</div>
))}
</div>
</div>
);
}<script setup>
import { ref } from "vue";
const props = defineProps({
items: {
type: Array,
default: () => [
{
label: "Finder",
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="4"/><path d="M3 9h18"/></svg>`,
},
{
label: "Mail",
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="4" width="20" height="16" rx="3"/><path d="m2 7 10 6 10-6"/></svg>`,
},
{
label: "Music",
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>`,
},
{
label: "Photos",
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="3"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>`,
},
{
label: "Messages",
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`,
},
{
label: "Calendar",
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="18" height="18" rx="3"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>`,
},
{
label: "Settings",
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>`,
},
],
},
baseSize: { type: Number, default: 48 },
maxSize: { type: Number, default: 72 },
magnifyRange: { type: Number, default: 200 },
});
const dockEl = ref(null);
const sizes = ref(props.items.map(() => props.baseSize));
const hoveredIndex = ref(null);
function handleMouseMove(e) {
if (!dockEl.value) return;
const children = Array.from(dockEl.value.children);
sizes.value = children.map((child) => {
const rect = child.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const distance = Math.abs(e.clientX - centerX);
const scale = Math.max(
props.baseSize,
props.maxSize -
((props.maxSize - props.baseSize) * Math.pow(distance, 2)) / Math.pow(props.magnifyRange, 2)
);
return Math.round(Math.min(props.maxSize, Math.max(props.baseSize, scale)));
});
}
function handleMouseLeave() {
sizes.value = props.items.map(() => props.baseSize);
hoveredIndex.value = null;
}
</script>
<template>
<div class="dock-wrapper">
<p class="dock-hint">Hover over the dock icons</p>
<div
ref="dockEl"
class="dock-bar"
@mousemove="handleMouseMove"
@mouseleave="handleMouseLeave"
>
<div
v-for="(item, i) in items"
:key="item.label"
class="dock-item"
@mouseenter="hoveredIndex = i"
@mouseleave="hoveredIndex = null"
@click="item.onClick?.()"
>
<div v-if="hoveredIndex === i" class="dock-tooltip">{{ item.label }}</div>
<div
class="dock-icon"
:style="{ width: sizes[i] + 'px', height: sizes[i] + 'px' }"
>
<div class="dock-icon-inner" v-html="item.icon"></div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.dock-wrapper {
min-height: 100vh;
background: #0a0a0a;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
padding: 2rem;
padding-bottom: 2rem;
font-family: system-ui, -apple-system, sans-serif;
}
.dock-hint {
color: #64748b;
font-size: 0.875rem;
margin-bottom: 3rem;
}
.dock-bar {
display: flex;
align-items: flex-end;
gap: 0.25rem;
padding: 0.625rem 0.875rem;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 1.25rem;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
}
.dock-item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
}
.dock-tooltip {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
background: rgba(15, 23, 42, 0.95);
color: #f1f5f9;
font-size: 0.75rem;
font-weight: 500;
padding: 0.3rem 0.6rem;
border-radius: 0.375rem;
border: 1px solid rgba(255, 255, 255, 0.1);
white-space: nowrap;
}
.dock-icon {
display: grid;
place-items: center;
border-radius: 0.75rem;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.3), rgba(168, 85, 247, 0.3));
border: 1px solid rgba(255, 255, 255, 0.1);
color: #e2e8f0;
transition: width 0.2s ease, height 0.2s ease;
}
.dock-icon-inner {
width: 55%;
height: 55%;
}
</style><script>
import { onMount } from "svelte";
export let items = [
{
label: "Finder",
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="4"/><path d="M3 9h18"/></svg>`,
},
{
label: "Mail",
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="4" width="20" height="16" rx="3"/><path d="m2 7 10 6 10-6"/></svg>`,
},
{
label: "Music",
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>`,
},
{
label: "Photos",
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="3"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>`,
},
{
label: "Messages",
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`,
},
{
label: "Calendar",
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="18" height="18" rx="3"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>`,
},
{
label: "Settings",
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>`,
},
];
export let baseSize = 48;
export let maxSize = 72;
export let magnifyRange = 200;
let dockEl;
let sizes = items.map(() => baseSize);
let hoveredIndex = null;
function handleMouseMove(e) {
if (!dockEl) return;
const children = Array.from(dockEl.children);
sizes = children.map((child) => {
const rect = child.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const distance = Math.abs(e.clientX - centerX);
const scale = Math.max(
baseSize,
maxSize - ((maxSize - baseSize) * Math.pow(distance, 2)) / Math.pow(magnifyRange, 2)
);
return Math.round(Math.min(maxSize, Math.max(baseSize, scale)));
});
}
function handleMouseLeave() {
sizes = items.map(() => baseSize);
hoveredIndex = null;
}
</script>
<div class="dock-wrapper">
<p class="dock-hint">Hover over the dock icons</p>
<div
class="dock-bar"
bind:this={dockEl}
on:mousemove={handleMouseMove}
on:mouseleave={handleMouseLeave}
>
{#each items as item, i}
<div
class="dock-item"
on:mouseenter={() => (hoveredIndex = i)}
on:mouseleave={() => (hoveredIndex = null)}
on:click={item.onClick}
>
{#if hoveredIndex === i}
<div class="dock-tooltip">{item.label}</div>
{/if}
<div
class="dock-icon"
style="width: {sizes[i]}px; height: {sizes[i]}px;"
>
<div class="dock-icon-inner">{@html item.icon}</div>
</div>
</div>
{/each}
</div>
</div>
<style>
.dock-wrapper {
min-height: 100vh;
background: #0a0a0a;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
padding: 2rem;
padding-bottom: 2rem;
font-family: system-ui, -apple-system, sans-serif;
}
.dock-hint {
color: #64748b;
font-size: 0.875rem;
margin-bottom: 3rem;
}
.dock-bar {
display: flex;
align-items: flex-end;
gap: 0.25rem;
padding: 0.625rem 0.875rem;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 1.25rem;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
}
.dock-item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
}
.dock-tooltip {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
background: rgba(15, 23, 42, 0.95);
color: #f1f5f9;
font-size: 0.75rem;
font-weight: 500;
padding: 0.3rem 0.6rem;
border-radius: 0.375rem;
border: 1px solid rgba(255, 255, 255, 0.1);
white-space: nowrap;
}
.dock-icon {
display: grid;
place-items: center;
border-radius: 0.75rem;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.3), rgba(168, 85, 247, 0.3));
border: 1px solid rgba(255, 255, 255, 0.1);
color: #e2e8f0;
transition: width 0.2s ease, height 0.2s ease;
}
.dock-icon-inner {
width: 55%;
height: 55%;
}
</style>macOS Dock
A faithful recreation of the macOS Dock magnification effect using vanilla CSS and JavaScript. Icons smoothly scale up based on their distance from the cursor, creating the signature fisheye lens feel.
How it works
- A horizontal flex container positions dock items along the bottom of the viewport.
- On
mousemove, the script calculates each icon’s center relative to the cursor X position. - A gaussian-like falloff function determines the scale factor: items closest to the cursor get the maximum scale, while distant items remain at base size.
- CSS
transitionontransformkeeps the scaling buttery smooth.
Features
- Smooth fisheye magnification with configurable scale and range
- Tooltip labels on hover
- Responsive — works on different viewport widths
prefers-reduced-motionrespected