# Dock 放大 Dock Magnification > 一排图标,鼠标靠近哪里哪里就鼓起来,旁边的被挤开,像苹果电脑的程序坞。 分类:动效 · 角色:点缀(贴在某个元素上,一页两三个) [全部组件和流程](https://www.dachuiai.com/llms.txt) · [动效选型规则](https://www.dachuiai.com/guides/motion.md) ## 给 AI:用户把这个组件发给了你 1. 先看用户的项目:框架、页面类型、现在的风格、手机还是电脑为主。 2. 对照下面的「适合 / 不适合」判断合不合适。不合适就直接说,并从[动效选型规则](https://www.dachuiai.com/guides/motion.md)或[全部组件](https://www.dachuiai.com/llms.txt)里另挑一个。 3. 合适就按「装进项目」装好,示例里的文字和图片换成用户自己的。 4. 告诉用户用了什么、放在哪个文件、以后想改快慢、颜色、大小改哪个参数。 ## 什么时候用 - 适合:个人主页、作品集的底部入口,5 到 9 个图标。 - 不适合:带文字的主导航(只有图标看不懂);以手机访问为主的页面。 - 适合的网站:作品集、官网 - 放在:导航 - 手机上:没效果:手机上就是一排普通图标,图标不小于 44px。 - 性能代价:低:只改 transform。 - 常搭配:[光标跟随 Custom Cursor](https://www.dachuiai.com/c/custom-cursor.md)、[磁吸 Magnetic](https://www.dachuiai.com/c/magnetic.md) ## 装进项目 - 写法:React 18 以上 + TypeScript + Tailwind CSS v4,一个文件就能用,不引用别的组件。 - 依赖:`npm i lucide-react` - 文件:原样存成 `dock.tsx`(比如 `src/components/dock.tsx`)。能跑命令就下载[原文](https://www.dachuiai.com/code/dock.tsx),比手抄准:`curl -fsSL -o src/components/dock.tsx https://www.dachuiai.com/code/dock.tsx`(Windows 写 `curl.exe`)。下载完看一眼第一行是 `import`,不是网页。 - 导出:`Dock` 是可以直接用的部分;最后的 `export default` 是演示示例,只作参考。 - 图片:示例用了本站的 [/art/peaks.jpg](https://www.dachuiai.com/art/peaks.jpg)。换成用户自己的图;只想先看效果,可以从本站同样的路径下载,放进项目的 public 目录。 - 保留代码里 `prefers-reduced-motion` 的处理:用户开了「减弱动态效果」时内容照样完整显示。 - Next.js(有 app 文件夹):文件第一行加 `'use client';`。 - 用 JavaScript 不用 TypeScript:Vite 和 Next.js 都能直接用 .tsx 文件;也可以删掉类型标注,改名 .jsx。 - 没有 Tailwind:先装 Tailwind CSS v4(Vite 项目 `npm i tailwindcss @tailwindcss/vite`,vite 配置的 plugins 里加 `tailwindcss()`,入口引入的那个 CSS 文件(一般是 `src/index.css`)第一行写 `@import "tailwindcss";`),或者把类名改写成项目用的样式。 - 已经装了 Tailwind v3:别为了一个组件升级。类名大多通用,少数 v4 写法要换,比如焦点框 `outline-2` 在 v3 里要写成 `outline outline-2`。 - 不是 React(Vue、Svelte、小程序、纯 HTML):照着代码的做法改写,时长、曲线、参数保持一样。Wix、有赞这类建站平台装不进代码,告诉用户去平台自带的动效设置里找。 ## 参数 | 参数 | 默认值 | 作用 | | --- | --- | --- | | `items` | | 要显示的一组内容,格式见代码里的类型 | | `size` | `56` | 图标的大小(px) | | `gap` | `10` | 间距(px) | | `magnify` | `1.9` | 鼠标正上方的图标放大几倍 | | `reach` | `150` | 鼠标多远以内的图标会跟着变大(px) | ## 你可以说 > 底部工具栏做成 Dock Magnification 程序坞放大:鼠标靠近哪里,哪里的图标就鼓起来,旁边的图标被挤开。 ## 完整代码 ```tsx import {useEffect, useRef, useState, type ComponentType} from 'react'; import {Calendar, Camera, Compass, FolderOpen, Mail, MessageCircle, Music, Settings, StickyNote} from 'lucide-react'; /* --------------------------------------------------------------- * Dock 放大 Dock Magnification * 鼠标靠近哪里,哪里的图标就鼓起来,旁边的图标被挤开,像苹果电脑的程序坞。 * 每个图标的放大倍数由它到鼠标的距离算出来,再把宽度累加, * 算出每个图标该待的位置;全部用 transform,页面布局不动。 * 键盘 Tab 到一个图标时,也以它为中心放大。 * ------------------------------------------------------------- */ type Icon = ComponentType<{size?: number; strokeWidth?: number; className?: string}>; export type DockItem = {label: string; icon: Icon; bg: string}; type Props = {items: DockItem[]; size?: number; gap?: number; magnify?: number; reach?: number}; export function Dock({items, size = 56, gap = 10, magnify = 1.9, reach = 150}: Props) { const bar = useRef(null); const bg = useRef(null); const els = useRef<(HTMLElement | null)[]>([]); const mouse = useRef(null); const [tip, setTip] = useState(-1); const n = items.length; const total = n * size + (n - 1) * gap; useEffect(() => { const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches; const cur = new Float32Array(n).fill(1); let raf = 0; const tick = () => { const background = bg.current; if (!background) return; raf = requestAnimationFrame(tick); const m = mouse.current; let sum = 0; for (let i = 0; i < n; i++) { const c = i * (size + gap) + size / 2; // 余弦曲线:鼠标正上方最大,越远越小,到 reach 处回到 1 const d = m == null ? reach : Math.min(reach, Math.abs(m - c)); const goal = reduce ? 1 : 1 + (magnify - 1) * (0.5 + 0.5 * Math.cos((d / reach) * Math.PI)); cur[i] += (goal - cur[i]) * 0.24; sum += cur[i] * size; } sum += (n - 1) * gap; let x = (total - sum) / 2; // 让鼓起来的这一排始终以中心为基准 for (let i = 0; i < n; i++) { const w = cur[i] * size, el = els.current[i]; if (el) el.style.transform = `translate(${x - i * (size + gap) + (w - size) / 2}px, ${-(w - size) / 2}px) scale(${cur[i]})`; x += w + gap; } background.style.transform = `scaleX(${(sum + 28) / (total + 28)})`; }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [n, size, gap, magnify, reach, total]); const local = (x: number) => x - bar.current!.getBoundingClientRect().left; return ( ); } /* ---------------- 示例:桌面 ---------------- */ const ITEMS: DockItem[] = [ {label: '访达', icon: FolderOpen, bg: 'linear-gradient(160deg,#5AC8FA,#0A84FF)'}, {label: '浏览器', icon: Compass, bg: 'linear-gradient(160deg,#64D2FF,#0071E3)'}, {label: '信息', icon: MessageCircle, bg: 'linear-gradient(160deg,#5EE27C,#28B446)'}, {label: '邮件', icon: Mail, bg: 'linear-gradient(160deg,#7AB8FF,#2F6BFF)'}, {label: '日历', icon: Calendar, bg: 'linear-gradient(160deg,#FF7A70,#E5342B)'}, {label: '音乐', icon: Music, bg: 'linear-gradient(160deg,#FF7E9B,#F0264F)'}, {label: '相机', icon: Camera, bg: 'linear-gradient(160deg,#6B7280,#2B2F38)'}, {label: '备忘录', icon: StickyNote, bg: 'linear-gradient(160deg,#FFD84D,#F5A623)'}, {label: '设置', icon: Settings, bg: 'linear-gradient(160deg,#A1A7B3,#5B616E)'}, ]; export default function DockDemo() { return (
); } ```