一、需求背景: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,可以快速输出多级产品分类菜单,无须额外开发整套导航系统。
但是,在实际项目中,原生组件的默认交互方式未必完全符合企业网站的使用需求。
本项目需要解决以下问题:
- 点击子菜单箭头时,第一次展开,第二次收起,箭头同步旋转。
- 当前页面属于深层产品分类时,自动展开所有上级分类,方便用户识别当前分类的层级位置。
- 父级分类名称始终保持正常链接行为,点击后直接跳转,不需要先展开子菜单。
- 子菜单展开和收起时增加平滑动画,避免菜单突然出现或消失。
- 所有修改仅作用于指定菜单,不影响网站其他 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 读取子菜单的实际高度,再将其转换为可以动画过渡的像素值。
展开过程如下:
- 将子菜单显示为
display: block。 - 将起始高度设置为
0px。 - 强制浏览器计算当前布局。
- 通过
scrollHeight获取子菜单的完整内容高度。 - 将高度从
0px过渡到实际像素高度。 - 动画结束后将高度恢复为
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 收起时反向执行高度过渡
收起动画的逻辑与展开相反:
- 获取当前子菜单的实际高度。
- 将当前高度锁定为像素值。
- 强制浏览器计算布局。
- 将高度过渡到
0px。 - 动画结束后再设置
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 动画。
最终实现了四个核心目标:
- 将父级分类链接和子菜单箭头的功能分离。
- 根据当前 WooCommerce 产品分类自动展开上级菜单。
- 通过 SVG 旋转实现箭头方向与展开状态同步。
- 通过 JavaScript 动态计算高度,实现平滑展开和收起,并在动画结束后恢复
height: auto。
对于使用 Elementor 构建的 WordPress 企业网站,这种方式适合需要保留原生组件、减少额外依赖,同时又希望提升导航交互体验的项目。
需要注意的是,Elementor 及其 SmartMenus 相关实现可能随版本更新而变化。升级后应重新测试箭头点击、父级导航、初始化展开和动画状态,确保自定义逻辑仍然兼容当前版本。
本文由 好主题 原创整理,致力于分享实用的 WordPress 建站知识与主题开发经验。 我们专注于提供高质量的 WordPress企业主题 资源,帮助中小企业轻松构建专业网站。 转载请注明来源,并保留原文链接,感谢您的支持与理解。