这是一个 hover 效果的菜单/列表,鼠标移到某一行时有三种效果:
- 黑色幕布自下而上划过整行
- 文字和箭头动效:文字向右平移,箭头向左上方抽走并旋转
- 鼠标旁边跟随显示图片预览,切换行时图片有擦拭过渡
本文用原生 HTML、CSS 和 JavaScript 实现,不依赖任何库,并说明如何放进 WordPress 页面。

效果演示
实现思路
- 黑色幕布:每一行里放一个绝对定位的
.hm-curtain,默认用clip-path: inset(100% 0 0 0)把它裁掉,hover 时变成inset(0 0 0 0),就形成自下而上的划过效果。 - 文字和箭头:hover 时用
transform平移文字、旋转并放大箭头,同时把文字颜色改成白色。 - 鼠标跟随预览:页面里放一个固定定位的
#hm-cursor,JavaScript 监听鼠标位置,用requestAnimationFrame配合线性插值平滑跟随。每一行通过data-image属性保存图片地址,hover 时切换预览图。
完整代码
HTML
把每个 data-image 里的"图片 URL"换成你自己的图片地址,href 换成要跳转的链接。需要更多行时,复制 <a>块即可。
<section class="hover-menu" aria-label="Interactive hover menu">
<div class="hover-menu__wrap">
<a class="hm-item" href="#t1" data-image="图片 URL_1">
<div class="hm-curtain" aria-hidden="true"></div>
<div class="hm-content">
<div class="hm-title">标题1</div>
<div class="hm-desc">这里是一行说明文字(分类/介绍)。</div>
</div>
<div class="hm-arrow" aria-hidden="true">
<svg class="hm-arrow__svg" viewBox="0 0 24 24" fill="none">
<path d="M7 17L17 7" />
<path d="M9 7h8v8" />
</svg>
</div>
</a>
<a class="hm-item" href="#t2" data-image="图片 URL_2">
<div class="hm-curtain" aria-hidden="true"></div>
<div class="hm-content">
<div class="hm-title">标题2</div>
<div class="hm-desc">这里是一行说明文字(分类/介绍)。</div>
</div>
<div class="hm-arrow" aria-hidden="true">
<svg class="hm-arrow__svg" viewBox="0 0 24 24" fill="none">
<path d="M7 17L17 7" />
<path d="M9 7h8v8" />
</svg>
</div>
</a>
<a class="hm-item" href="#t3" data-image="图片 URL_3">
<div class="hm-curtain" aria-hidden="true"></div>
<div class="hm-content">
<div class="hm-title">标题3</div>
<div class="hm-desc">这里是一行说明文字(分类/介绍)。</div>
</div>
<div class="hm-arrow" aria-hidden="true">
<svg class="hm-arrow__svg" viewBox="0 0 24 24" fill="none">
<path d="M7 17L17 7" />
<path d="M9 7h8v8" />
</svg>
</div>
</a>
</div>
<!-- 鼠标跟随预览容器 -->
<div id="hm-cursor" class="hm-cursor" aria-hidden="true"></div>
</section>CSS
/* ========== Layout ========== */
.hover-menu{
width: 100%;
max-width: 980px;
margin: 48px auto;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
}
.hover-menu__wrap{
border-top: 1px solid rgba(0,0,0,.15);
}
/* ========== Menu Items ========== */
.hm-item{
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: clamp(16px, 2vw, 28px);
padding: clamp(16px, 1.2vw + 12px, 28px) 6px;
border-bottom: 1px solid rgba(0,0,0,.15);
text-decoration: none;
color: #111;
overflow: hidden;
}
.hm-content{
position: relative;
z-index: 2;
max-width: 70ch; /* 防止说明太长撑爆 */
}
.hm-title{
font-size: clamp(26px, 3.2vw + 10px, 56px); /* 流体字号:手机小、桌面大 */
line-height: 1.05;
letter-spacing: -0.02em;
transition: color .25s ease;
}
.hm-desc{
margin-top: 10px;
font-size: clamp(13px, 0.55vw + 11px, 16px);
line-height: 1.45;
color: rgba(0,0,0,.6);
transition: color .25s ease;
}
.hm-arrow{
position: relative;
z-index: 2;
width: 44px;
height: 44px;
display: grid;
place-items: center;
transform: translate3d(0,0,0);
transition: transform .25s ease, color .25s ease;
user-select: none;
}
.hm-arrow__svg{
width: 22px;
height: 22px;
stroke: currentColor;
stroke-width: 2.2;
stroke-linecap: round;
stroke-linejoin: round;
}
/* 黑幕 curtain:默认裁掉;hover 时显示 */
.hm-curtain{
position: absolute;
inset: 0;
background: #111;
z-index: 1;
pointer-events: none;
clip-path: inset(100% 0 0 0);
transition: clip-path .45s cubic-bezier(.25,1,.5,1);
}
.hm-item:hover .hm-curtain{ clip-path: inset(0 0 0 0); }
.hm-item:hover .hm-title{ color: #fff; }
.hm-item:hover .hm-desc{ color: rgba(255,255,255,.65); }
.hm-item:hover .hm-content{ transform: translateX(18px); transition: transform .25s ease; }
.hm-item:hover .hm-arrow{
color: #fff;
transform: translate(-14px, 0) rotate(45deg) scale(1.08);
}
/* ========== Cursor Preview ========== */
.hm-cursor{
position: fixed;
top: 0; left: 0;
width: 320px;
height: 220px;
border-radius: 14px;
overflow: hidden;
background: #111;
box-shadow: 0 30px 60px rgba(0,0,0,.25);
pointer-events: none;
z-index: 9999;
opacity: 0;
transform: translate3d(0,0,0) scale(.92);
transition: opacity .25s ease;
display: none; /* 移动端默认不显示 */
}
@media (min-width: 768px){
.hm-cursor{ display: block; }
}
.hm-cursor img{
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
/* 图片切换的"擦拭"动画(进/出) */
@keyframes hmWipeIn{
from { clip-path: inset(100% 0 0 0); transform: scale(1.02); }
to { clip-path: inset(0 0 0 0); transform: scale(1); }
}
@keyframes hmWipeOut{
from { clip-path: inset(0 0 0 0); opacity: 1; }
to { clip-path: inset(0 0 100% 0); opacity: 0.9; }
}
.hm-img-enter{ animation: hmWipeIn .65s cubic-bezier(.25,1,.5,1) both; z-index: 2; }
.hm-img-exit { animation: hmWipeOut .65s cubic-bezier(.25,1,.5,1) both; z-index: 1; }
/* 减少动态:尊重系统设置 */
@media (prefers-reduced-motion: reduce){
.hm-curtain, .hm-arrow, .hm-title, .hm-desc { transition: none !important; }
.hm-img-enter, .hm-img-exit { animation: none !important; }
}JavaScript
<script>
(() => {
const cursor = document.getElementById('hm-cursor');
const items = document.querySelectorAll('.hm-item');
if (!cursor || !items.length) return;
// 跟随参数
const CURSOR_W = 320; // 需与 CSS 中 .hm-cursor 的 width 一致
const CURSOR_H = 220; // 需与 CSS 中 .hm-cursor 的 height 一致
const PAD = 24; // 预览框与视口边缘的最小距离
const SHIFT_X = 360; // 预览框在鼠标右侧的偏移
const LERP = 0.12; // 跟随平滑度,越小越慢
let mouseX = 0, mouseY = 0;
let x = 0, y = 0;
let hovering = false;
let activeSrc = null;
let rafId = null;
// 预加载图片(避免 hover 时空白)
items.forEach(a => {
const url = a.getAttribute('data-image');
if (url) { const img = new Image(); img.src = url; }
});
const onMove = (e) => { mouseX = e.clientX; mouseY = e.clientY; };
document.addEventListener('mousemove', onMove, { passive: true });
items.forEach(item => {
item.addEventListener('mouseenter', () => {
hovering = true;
cursor.style.opacity = '1';
const url = item.getAttribute('data-image');
if (url) swapImage(url);
start();
});
item.addEventListener('mouseleave', () => {
hovering = false;
cursor.style.opacity = '0';
// 鼠标移开后停止动画循环,避免持续占用 CPU
setTimeout(() => {
if (!hovering && rafId) { cancelAnimationFrame(rafId); rafId = null; }
}, 400);
});
// 键盘 focus 也能看见(无鼠标时不跟随,只显示在右侧固定位置)
item.addEventListener('focus', () => {
const url = item.getAttribute('data-image');
if (url) swapImage(url);
hovering = true;
cursor.style.opacity = '1';
// 固定放到视口右侧中间
x = Math.min(window.innerWidth - CURSOR_W - PAD, window.innerWidth * 0.55);
y = Math.max(PAD, (window.innerHeight - CURSOR_H) / 2);
cursor.style.transform = `translate3d(${x}px, ${y}px, 0) scale(1)`;
});
item.addEventListener('blur', () => {
hovering = false;
cursor.style.opacity = '0';
});
});
function start(){
if (rafId) return;
rafId = requestAnimationFrame(tick);
}
function tick(){
// 目标位置:鼠标右侧 + 垂直居中
let tx = mouseX + SHIFT_X;
let ty = mouseY - CURSOR_H / 2;
// 防止出屏(右边和上下)
const maxLeft = window.innerWidth - CURSOR_W - PAD;
const maxTop = window.innerHeight - CURSOR_H - PAD;
tx = Math.max(PAD, Math.min(tx, maxLeft));
ty = Math.max(PAD, Math.min(ty, maxTop));
// 平滑跟随
x += (tx - x) * LERP;
y += (ty - y) * LERP;
const scale = hovering ? 1 : 0.92;
cursor.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`;
rafId = requestAnimationFrame(tick);
}
function swapImage(url){
if (activeSrc === url) return;
activeSrc = url;
const img = document.createElement('img');
img.src = url;
img.alt = ""; // 装饰性预览图
img.className = 'hm-img-enter';
// 旧图出场
[...cursor.children].forEach(old => {
old.classList.remove('hm-img-enter');
old.classList.add('hm-img-exit');
setTimeout(() => old.remove(), 700);
});
cursor.appendChild(img);
}
// 视口变化时避免出屏
window.addEventListener('resize', () => {
x = Math.min(x, window.innerWidth - CURSOR_W - PAD);
y = Math.min(y, window.innerHeight - CURSOR_H - PAD);
}, { passive: true });
})();
</script>在 WordPress 中使用
- 编辑要放菜单的页面或文章,添加一个「自定义 HTML」块。
- 在这个块里依次放入三部分内容:
- 用
<style>标签包住上面的 CSS:
- 用
<style>
/* 把上面的 CSS 粘贴在这里 */
</style>- 上面的 HTML
- 上面的 JavaScript(已经带
<script>标签)
- 把每个
data-image里的"图片 URL"换成媒体库里图片的文件地址(在媒体库里点开图片,复制「文件 URL」,通常以https://你的域名/wp-content/uploads/开头)。 - 把
href换成要跳转的页面或文章链接。 - 编辑器里不一定能看到完整效果,请在「预览」或前台页面查看。
几点说明:
- 如果
<script>被过滤掉了,通常是当前用户没有「unfiltered_html」权限。多站点环境下,只有超级管理员才有。 - 需要在多个页面使用时,可以把 CSS 放到站点的「额外 CSS」里,不必每页都粘贴一遍。
- 想让菜单自动显示最新文章,而不是手写每一行,见第二篇。
自定义
- 预览框大小:同时修改 CSS 中
.hm-cursor的width、height和 JS 里的CURSOR_W、CURSOR_H,两边必须一致,否则预览框可能超出屏幕边缘。 - 预览框位置:
SHIFT_X决定它出现在鼠标右侧多远。 - 跟随速度:
LERP越小越慢、越"粘",越大越跟手。 - 配色:幕布颜色改
.hm-curtain的background,hover 时的文字颜色改.hm-item:hover .hm-title等规则。 - 标题字号:改
.hm-title的clamp()三个值(最小、随屏幕变化的值、最大)。
常见问题
hover 时预览图不显示?
图片地址和 DOM 都正常的情况下,可以检查预览容器的实际宽度,见 WordPress|hover 菜单预览图不显示:元素宽度为 0 的排查与修复。
想让菜单自动显示最新文章?
见 WordPress|hover 菜单自动显示最新文章(REST API)。
手机上为什么没有预览图?
触屏设备没有 hover,所以 CSS 里在 768px 以下把预览容器隐藏了,菜单本身的点击跳转不受影响。
键盘能操作吗?
可以。用 Tab 键切换到某一行时,也会在屏幕右侧显示预览图。系统开启"减少动态"时,所有过渡动画会自动关闭。