Html+CSS|WordPress制作 hover效果菜单:黑幕过场+鼠标跟随预览

这是一个 hover 效果的菜单/列表,鼠标移到某一行时有三种效果:

  • 黑色幕布自下而上划过整行
  • 文字和箭头动效:文字向右平移,箭头向左上方抽走并旋转
  • 鼠标旁边跟随显示图片预览,切换行时图片有擦拭过渡

本文用原生 HTML、CSS 和 JavaScript 实现,不依赖任何库,并说明如何放进 WordPress 页面。

hover 菜单效果:黑色幕布划过菜单行,鼠标旁边显示图片预览

效果演示

实现思路

  • 黑色幕布:每一行里放一个绝对定位的 .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 中使用

  1. 编辑要放菜单的页面或文章,添加一个「自定义 HTML」块。
  2. 在这个块里依次放入三部分内容:
    • 用 <style> 标签包住上面的 CSS:
     <style>
     /* 把上面的 CSS 粘贴在这里 */
     </style>
  • 上面的 HTML
  • 上面的 JavaScript(已经带 <script> 标签)
  1. 把每个 data-image 里的"图片 URL"换成媒体库里图片的文件地址(在媒体库里点开图片,复制「文件 URL」,通常以 https://你的域名/wp-content/uploads/ 开头)。
  2. 把 href 换成要跳转的页面或文章链接。
  3. 编辑器里不一定能看到完整效果,请在「预览」或前台页面查看。

几点说明:

  • 如果 <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 键切换到某一行时,也会在屏幕右侧显示预览图。系统开启"减少动态"时,所有过渡动画会自动关闭。