这个 hover 效果的原理:
1,每一行的 hover 过场(黑色背景从下往上“盖住”)+ 文字/箭头动效(纯 CSS)
2,鼠标旁边跟随的图片预览浮层(JS 监听鼠标 + data-image 切图)
以下为HTML/CSS/JS的实现方案
HTML(选几张图片 URL 填进去)
<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">↗</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">↗</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">↗</div>
</a>
<a class="hm-item" href="#t4" data-image="图片 URL_4">
<div class="hm-curtain" aria-hidden="true"></div>
<div class="hm-content">
<div class="hm-title">标题4</div>
<div class="hm-desc">这里是一行说明文字(分类/介绍)。</div>
</div>
<div class="hm-arrow" aria-hidden="true">↗</div>
</a>
<a class="hm-item" href="#t5" data-image="图片 URL_5">
<div class="hm-curtain" aria-hidden="true"></div>
<div class="hm-content">
<div class="hm-title">标题5</div>
<div class="hm-desc">这里是一行说明文字(分类/介绍)。</div>
</div>
<div class="hm-arrow" aria-hidden="true">↗</div>
</a>
</div>
<!-- 鼠标跟随预览容器 -->
<div id="hm-cursor" class="hm-cursor" aria-hidden="true"></div>
</section>
CSS(黑幕过场 + 文字/箭头动效 + 浮动图片)
/* ========== Layout ========== */
.hover-menu{
width: min(980px, 92vw);
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: 24px;
padding: 22px 6px;
border-bottom: 1px solid rgba(0,0,0,.15);
text-decoration: none;
color: #111;
overflow: hidden;
}
.hm-content{ position: relative; z-index: 2; }
.hm-title{
font-size: 40px;
line-height: 1.05;
letter-spacing: -0.02em;
transition: color .25s ease;
}
.hm-desc{
margin-top: 10px;
font-size: 14px;
line-height: 1.4;
color: rgba(0,0,0,.6);
transition: color .25s ease;
}
.hm-arrow{
position: relative;
z-index: 2;
font-size: 22px;
transform: translate3d(0,0,0);
transition: transform .25s ease, color .25s ease;
user-select: none;
}
/* 黑幕 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; }
}
.hm-item{
padding: clamp(16px, 1.2vw + 12px, 28px) 6px;
gap: clamp(16px, 2vw, 28px);
}
.hm-content{
max-width: 70ch; /* 防止说明太长撑爆 */
}
JS(监听 hover + 鼠标跟随 + 切图)
<script>
(() => {
const cursor = document.getElementById('hm-cursor');
const items = document.querySelectorAll('.hm-item');
if (!cursor || !items.length) return;
// 跟随参数
const CURSOR_W = 320;
const CURSOR_H = 220;
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';
});
// 键盘 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>
效果
优化方案:
1,把箭头文字 ↗ 换成 SVG箭头
2,标题字号自适应:用 clamp() 做流体排版
把每一行里的:
<div class=”hm-arrow” aria-hidden=”true”>↗</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>
追加到现有 CSS 里:
/* 标题:流体字号(手机小、桌面大) */
.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;
}
/* SVG 箭头 */
.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;
}
/* hover 时箭头更像示例:向左上“抽走”+ 轻微旋转放大 */
.hm-item:hover .hm-arrow{
color: #fff;
transform: translate(-14px, 0) rotate(45deg) scale(1.08);
}
