# 悬停预览 Hover Reveal > 鼠标移到列表的哪一行,这一行的图片就跟着鼠标冒出来,其他行变淡。 分类:动效 · 角色:点缀(贴在某个元素上,一页两三个) [全部组件和流程](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. 告诉用户用了什么、放在哪个文件、以后想改快慢、颜色、大小改哪个参数。 ## 什么时候用 - 适合:作品、项目、文章、菜单这类纯文字列表,想要高级感又不想摆一堆图。 - 不适合:商品列表(顾客要直接看到图);以手机访问为主的页面。 - 适合的网站:作品集、官网、内容站 - 放在:列表 - 手机上:要改:手机上没有悬停,图永远看不到;手机上在每行旁边直接放小图。 - 性能代价:低:图片跟着鼠标只改 transform。 - 常搭配:[光标跟随 Custom Cursor](https://www.dachuiai.com/c/custom-cursor.md)、[乱码解码 Scramble Text](https://www.dachuiai.com/c/scramble-text.md) ## 装进项目 - 写法:React 18 以上 + TypeScript + Tailwind CSS v4,一个文件就能用,不引用别的组件。 - 依赖:`npm i lucide-react` - 文件:原样存成 `hover-reveal.tsx`(比如 `src/components/hover-reveal.tsx`)。能跑命令就下载[原文](https://www.dachuiai.com/code/hover-reveal.tsx),比手抄准:`curl -fsSL -o src/components/hover-reveal.tsx https://www.dachuiai.com/code/hover-reveal.tsx`(Windows 写 `curl.exe`)。下载完看一眼第一行是 `import`,不是网页。 - 导出:`HoverRevealList` 是可以直接用的部分;最后的 `export default` 是演示示例,只作参考。 - 图片:示例用了本站的 [/art/ocean.jpg](https://www.dachuiai.com/art/ocean.jpg)、[/art/planet.jpg](https://www.dachuiai.com/art/planet.jpg)、[/art/dunes.jpg](https://www.dachuiai.com/art/dunes.jpg)、[/art/lava.jpg](https://www.dachuiai.com/art/lava.jpg)、[/art/fluid-rose.jpg](https://www.dachuiai.com/art/fluid-rose.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` | | 要显示的一组内容,格式见代码里的类型 | ## 你可以说 > 作品列表加 Hover Reveal 悬停预览:鼠标移到哪一行,这一行的图片就跟着鼠标冒出来,其他行变淡。 ## 完整代码 ```tsx import {useEffect, useRef, useState} from 'react'; import {ArrowRight} from 'lucide-react'; /* --------------------------------------------------------------- * HoverReveal 悬停预览 * 鼠标移到文字上,图片跟着冒出来。一排纯文字,一下高级了。 * 图片慢半拍跟着鼠标,甩得快时还会歪一点;从中间裁开着出现。 * 键盘 Tab 到一行时,图片出现在这一行的右边。 * ------------------------------------------------------------- */ export type RevealItem = {title: string; meta: string; img: string}; export function HoverRevealList({items}: {items: RevealItem[]}) { const root = useRef(null); const float = useRef(null); const [active, setActive] = useState(-1); useEffect(() => { const el = root.current!; const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches; const m = {x: 0, y: 0}, c = {x: 0, y: 0, a: 0}; let raf = 0, seen = false; const tick = () => { const preview = float.current; if (!preview) return; raf = requestAnimationFrame(tick); const k = reduce ? 1 : 0.11; const dx = m.x - c.x; c.x += dx * k; c.y += (m.y - c.y) * k; c.a += (Math.max(-14, Math.min(14, dx * 0.06)) - c.a) * 0.12; preview.style.transform = `translate(${c.x}px, ${c.y}px) translate(-50%, -50%) rotate(${reduce ? 0 : c.a}deg)`; }; const move = (e: PointerEvent) => { const r = el.getBoundingClientRect(); m.x = e.clientX - r.left; m.y = e.clientY - r.top; if (!seen) { seen = true; c.x = m.x; c.y = m.y; } }; const focus = (e: FocusEvent) => { const row = (e.target as HTMLElement).closest('a'); if (!row) return; const r = row.getBoundingClientRect(), b = el.getBoundingClientRect(); m.x = r.right - b.left - 200; m.y = r.top - b.top + r.height / 2; if (!seen) { seen = true; c.x = m.x; c.y = m.y; } }; el.addEventListener('pointermove', move); el.addEventListener('focusin', focus); raf = requestAnimationFrame(tick); return () => { cancelAnimationFrame(raf); el.removeEventListener('pointermove', move); el.removeEventListener('focusin', focus); }; }, []); return (
setActive(-1)}> {/* 浮动的图片:所有图都叠在一起,只显示当前那张 */}
{items.map((it, i) => (
))}
); } const HR_CSS = ` .hr-img { clip-path: inset(50% 0 50% 0 round 16px); transition: clip-path .45s cubic-bezier(.22,1,.36,1); } .hr-img img { transform: scale(1.3); transition: transform .8s cubic-bezier(.22,1,.36,1); } .hr-on { clip-path: inset(0 0 0 0 round 16px); z-index: 1; } .hr-on img { transform: none; } @media (prefers-reduced-motion: reduce) { .hr-img, .hr-img img { transition: none; } } `; /* ---------------- 示例:作品列表 ---------------- */ const ITEMS: RevealItem[] = [ {title: '海面之下', meta: '摄影 · 2026', img: '/art/ocean.jpg'}, {title: '远行星', meta: '插画 · 2025', img: '/art/planet.jpg'}, {title: '第七天的沙', meta: '纪录片 · 2025', img: '/art/dunes.jpg'}, {title: '地火', meta: '装置 · 2024', img: '/art/lava.jpg'}, {title: '颜料的河', meta: '动态影像 · 2024', img: '/art/fluid-rose.jpg'}, ]; export default function HoverRevealDemo() { return (
); } ```