欢迎光临好主题官网!致力于 WordPress 企业主题定制开发及 WP外贸模板下载。

Elementor 导航菜单自定义:子菜单展开动画、箭头旋转与分类自动展开

文章目录

一、需求背景:Elementor 原生导航菜单的交互优化

在 WordPress 企业网站和 WooCommerce 商城项目中,产品分类通常具有多级层级结构。例如,一个化学品企业网站可能包含以下分类:

Life Science Chemicals
├── Biochemicals
└── Other Life Science Chemicals
    └── Pharmaceutical Intermediates

Material Chemicals
├── Chemical Reagents
├── Cosmetic Chemicals
├── Industrial Chemicals
└── Other Material Chemicals

使用 Elementor 原生 Nav Menu 组件,将布局设置为 Dropdown,可以快速输出多级产品分类菜单,无须额外开发整套导航系统。

但是,在实际项目中,原生组件的默认交互方式未必完全符合企业网站的使用需求。

本项目需要解决以下问题:

  1. 点击子菜单箭头时,第一次展开,第二次收起,箭头同步旋转。
  2. 当前页面属于深层产品分类时,自动展开所有上级分类,方便用户识别当前分类的层级位置。
  3. 父级分类名称始终保持正常链接行为,点击后直接跳转,不需要先展开子菜单。
  4. 子菜单展开和收起时增加平滑动画,避免菜单突然出现或消失。
  5. 所有修改仅作用于指定菜单,不影响网站其他 Elementor 导航组件。

最终方案保留 Elementor 原生 Nav Menu,不替换菜单 HTML 结构,也不引入额外的前端轮播或菜单库。

二、为什么选择 Elementor 原生菜单进行改造?

Elementor 原生导航菜单已经提供了基础的菜单结构、子菜单标记、ARIA 属性以及 SmartMenus 相关交互逻辑。

例如,具有子菜单的链接通常包含:

<a class="elementor-item has-submenu"
   aria-expanded="false">
    Life Science Chemicals
    <span class="sub-arrow">
        <!-- SVG 箭头 -->
    </span>
</a>

子菜单则通过 ul.sub-menu 输出。

这意味着,我们可以在不重建菜单系统的情况下,利用现有 DOM 结构实现交互增强。

本项目为目标菜单添加了自定义 CSS 类:

.hzt-products-menu

之后所有自定义样式和 JavaScript 都限定在该容器内,避免影响页眉主导航、移动端导航或网站其他菜单。

这种做法更适合需要长期维护的 WordPress 企业主题项目。

三、实现方案与核心原理

整个实现分为三个部分。

3.1 使用 JavaScript 控制子菜单状态

通过 aria-expanded 判断当前菜单状态:

  • true:子菜单处于展开状态。
  • false:子菜单处于收起状态。

点击箭头时,根据当前状态切换子菜单,并同步更新 ARIA 属性和 highlighted 类。

同时,使用 event.preventDefault() 阻止箭头触发链接跳转,使用 event.stopImmediatePropagation() 阻止 Elementor / SmartMenus 的后续点击处理程序再次执行,从而避免原生脚本重复切换菜单。

需要注意,这类事件拦截依赖 Elementor 当前的 DOM 结构和事件处理方式。如果升级 Elementor 后出现行为变化,应重新验证事件处理逻辑。

3.2 自动展开当前分类的所有父级菜单

WooCommerce 产品分类页面通常会通过菜单项上的状态类标记当前分类,例如:

.current-menu-item
.current-product_cat
.current-menu-parent

初始化时,JavaScript 首先定位当前分类,再从当前菜单项向上遍历 DOM 祖先节点。

当找到带有子菜单的父级分类时,调用统一的菜单状态控制函数展开子菜单。

例如,当前页面为 Pharmaceutical Intermediates,页面加载后将自动展开:

Life Science Chemicals
    └── Other Life Science Chemicals
        └── Pharmaceutical Intermediates  ← 当前分类

这样用户进入深层分类页面时,可以直接看到当前分类所属的完整菜单路径。

3.3 通过 SVG 旋转实现箭头动画

Elementor 原生菜单的下拉箭头使用 SVG 图标,可以通过 CSS 的 transform: rotate() 控制方向。

收起状态:

.hzt-products-menu a.has-submenu[aria-expanded="false"] .sub-arrow svg {
    transform: rotate(0deg) !important;
}

展开状态:

.hzt-products-menu a.has-submenu[aria-expanded="true"] .sub-arrow svg {
    transform: rotate(180deg) !important;
}

再配合 transition,即可实现箭头的平滑旋转。

四、关键问题:如何让子菜单实现平滑展开和收起?

这是本次优化中最重要的部分。

4.1 为什么不能直接设置 height: 0 到 height: auto?

CSS 可以直接设置:

height: 0;
height: auto;

但是,传统 CSS Transition 无法直接在 0 和 auto 之间计算连续的中间高度,因此不能仅靠这两个值实现标准的高度过渡动画。

如果直接设置 display: none 和 display: block,菜单通常会突然出现或消失,也无法形成传统的高度展开效果。

4.2 解决方案:JavaScript 动态计算真实高度

本项目保留原有的 display: block / none 控制逻辑,通过 JavaScript 读取子菜单的实际高度,再将其转换为可以动画过渡的像素值。

展开过程如下:

  1. 将子菜单显示为 display: block。
  2. 将起始高度设置为 0px。
  3. 强制浏览器计算当前布局。
  4. 通过 scrollHeight 获取子菜单的完整内容高度。
  5. 将高度从 0px 过渡到实际像素高度。
  6. 动画结束后将高度恢复为 auto,适应后续的响应式布局。

核心代码如下:

function animateOpen(submenu) {
    submenu.style.display = 'block';
    submenu.style.overflow = 'hidden';
    submenu.style.height = '0px';
    submenu.style.opacity = '0';

    // 强制浏览器应用起始状态
    void submenu.offsetHeight;

    const targetHeight = submenu.scrollHeight;

    submenu.style.height = targetHeight + 'px';
    submenu.style.opacity = '1';

    function onEnd(event) {
        if (
            event.target !== submenu ||
            event.propertyName !== 'height'
        ) {
            return;
        }

        submenu.removeEventListener('transitionend', onEnd);

        if (submenu.getAttribute('aria-hidden') === 'false') {
            submenu.style.height = 'auto';
            submenu.style.overflow = '';
            submenu.style.opacity = '';
        }
    }

    submenu.addEventListener('transitionend', onEnd);
}

这里有两个关键点。

第一,scrollHeight 用于获取完整内容高度。

它能够帮助我们计算子菜单展开后需要占据的实际高度,而不是使用固定的 300px、500px 等数值。

第二,动画结束后恢复 height: auto。

这样可以避免子菜单长期锁定在某个像素高度,减少窗口尺寸变化、字体换行或菜单内容调整带来的布局问题。

4.3 收起时反向执行高度过渡

收起动画的逻辑与展开相反:

  1. 获取当前子菜单的实际高度。
  2. 将当前高度锁定为像素值。
  3. 强制浏览器计算布局。
  4. 将高度过渡到 0px。
  5. 动画结束后再设置 display: none。

核心代码如下:

function animateClose(submenu) {
    if (getComputedStyle(submenu).display === 'none') {
        submenu.style.height = '0px';
        submenu.style.opacity = '0';
        return;
    }

    const currentHeight = submenu.getBoundingClientRect().height;

    submenu.style.height = currentHeight + 'px';
    submenu.style.overflow = 'hidden';
    submenu.style.opacity = '1';

    // 强制浏览器应用当前高度
    void submenu.offsetHeight;

    submenu.style.height = '0px';
    submenu.style.opacity = '0';

    function onEnd(event) {
        if (
            event.target !== submenu ||
            event.propertyName !== 'height'
        ) {
            return;
        }

        submenu.removeEventListener('transitionend', onEnd);

        if (submenu.getAttribute('aria-hidden') === 'true') {
            submenu.style.display = 'none';
            submenu.style.height = '0px';
            submenu.style.overflow = '';
            submenu.style.opacity = '0';
        }
    }

    submenu.addEventListener('transitionend', onEnd);
}

这样就能在不改变原有 display 控制逻辑的情况下,完成子菜单的平滑收起。

五、CSS 动画样式

为子菜单添加高度和透明度的过渡效果:

/* 仅作用于产品分类菜单 */
.hzt-products-menu ul.sub-menu.hzt-submenu-animating {
    overflow: hidden;
    transition: height 0.28s ease, opacity 0.22s ease;
}

/* 箭头旋转动画 */
.hzt-products-menu .sub-arrow svg {
    transform: rotate(0deg) !important;
    transition: transform 0.25s ease;
    transform-origin: center;
}

/* 展开状态:箭头向上 */
.hzt-products-menu a.has-submenu[aria-expanded="true"] .sub-arrow svg {
    transform: rotate(180deg) !important;
}

/* 收起状态:箭头向下 */
.hzt-products-menu a.has-submenu[aria-expanded="false"] .sub-arrow svg {
    transform: rotate(0deg) !important;
}

/* 尊重用户的减少动态效果设置 */
@media (prefers-reduced-motion: reduce) {
    .hzt-products-menu ul.sub-menu.hzt-submenu-animating,
    .hzt-products-menu .sub-arrow svg {
        transition: none !important;
    }
}

其中,子菜单高度动画约为 0.28s,透明度动画约为 0.22s,箭头旋转约为 0.25s。

这些数值可以根据网站整体交互风格调整。

六、如何处理父级分类链接与箭头的点击冲突?

这是 Elementor 原生下拉菜单改造时需要特别注意的问题。

如果父级分类同时具有链接和子菜单,原生 SmartMenus 可能会把第一次点击解释为展开子菜单,第二次点击才执行跳转。

本项目希望实现的交互规则是:

用户操作 执行结果
点击父级分类文字 直接访问分类页面
点击下拉箭头 展开子菜单
再次点击下拉箭头 收起子菜单
点击其他父级箭头 独立控制对应子菜单

因此,需要在菜单容器上监听点击事件,并通过事件目标区分 .sub-arrow 与 a.has-submenu。

对于箭头点击,拦截默认链接行为,再手动切换子菜单。

对于父级链接文字,则阻止 SmartMenus 的后续事件处理,但不调用 preventDefault(),使浏览器继续执行原始链接导航。

这就实现了“文字负责跳转,箭头负责展开和收起”的分离式交互。

七、如何避免影响其他 Elementor 导航菜单?

本项目使用独立的 CSS 类作为作用范围:

.hzt-products-menu

所有事件监听都绑定在该菜单容器上,样式也都以该类作为选择器前缀。

这样做的好处包括:

  • 不需要修改 Elementor 插件源码。
  • 不需要重建 WordPress 菜单。
  • 不影响其他使用相同 Nav Menu 组件的区域。
  • 后续更换菜单内容时,通常不需要修改动画代码。
  • 可以在不同产品分类模板中复用同一套实现。

如果网站有多个需要使用该效果的产品分类菜单,只需给对应的 Elementor Nav Menu 组件添加相同的自定义类即可。

八、实际部署与测试流程

建议在测试环境完成以下检查后,再应用到生产网站。

第一步:添加自定义类

在 Elementor 编辑器中选中目标 Nav Menu 组件,在“高级”设置中添加:

hzt-products-menu

不要输入前面的点号。

第二步:添加 CSS 和 JavaScript

将样式添加到 Elementor 自定义 CSS 或主题样式文件中。

将 JavaScript 放在 Elementor 自定义代码、主题前端 JS 文件或自定义核心插件中。

如果使用 Elementor 自定义代码,建议在页面底部加载,并检查页面上是否已经存在旧版本代码,避免重复绑定事件。

第三步:清理缓存

如果网站使用 WP Rocket 等缓存插件,更新代码后需要清除相关缓存。

测试时建议使用浏览器无痕窗口,避免旧脚本或旧 CSS 干扰结果。

第四步:验证核心场景

  • 点击箭头可以展开子菜单。
  • 再次点击箭头可以收起子菜单。
  • 展开后箭头向上,收起后箭头向下。
  • 父级分类文字第一次点击即可跳转。
  • 深层产品分类页面刷新后自动展开所有父级。
  • 收起外层菜单时,内部子菜单同步重置。
  • 动画不会导致菜单内容被截断。
  • 网站其他 Elementor 导航菜单不受影响。

九、总结

本次优化没有替换 Elementor 原生 Nav Menu,也没有引入额外菜单插件,而是在原有 DOM 结构上增加了必要的 JavaScript 状态控制和 CSS 动画。

最终实现了四个核心目标:

  1. 将父级分类链接和子菜单箭头的功能分离。
  2. 根据当前 WooCommerce 产品分类自动展开上级菜单。
  3. 通过 SVG 旋转实现箭头方向与展开状态同步。
  4. 通过 JavaScript 动态计算高度,实现平滑展开和收起,并在动画结束后恢复 height: auto。

对于使用 Elementor 构建的 WordPress 企业网站,这种方式适合需要保留原生组件、减少额外依赖,同时又希望提升导航交互体验的项目。

需要注意的是,Elementor 及其 SmartMenus 相关实现可能随版本更新而变化。升级后应重新测试箭头点击、父级导航、初始化展开和动画状态,确保自定义逻辑仍然兼容当前版本。

本文由 好主题 原创整理,致力于分享实用的 WordPress 建站知识与主题开发经验。 我们专注于提供高质量的 WordPress企业主题 资源,帮助中小企业轻松构建专业网站。 转载请注明来源,并保留原文链接,感谢您的支持与理解。

联系我们

教程看不懂?联系我们免费为您解答!免费助力个人,小企站点!

客服微信
相关文章
主题推荐

会员注册

成为会员,获得更多专属优惠!

验证码: 加载中... =

已有账号?
还没有账号?