欢迎访问晨星博客!
桌面导航切换到移动菜单,难点通常不在“把横向菜单隐藏起来”,而在于明确每一层代码应该负责什么:CSS 负责布局、显示状态和过渡效果,JavaScript 只负责用户触发的开关、状态同步以及必要的交互反馈。职责划分清楚后,导航栏在 WordPress 主题或普通前端页面中都更容易维护。

在开始写代码前,先把导航栏看成两种主要状态,而不是两套完全独立的组件。
宽屏状态下,品牌区域通常位于左侧,导航项横向排列在右侧或居中位置。菜单按钮不需要出现,导航链接默认可见,用户可以直接点击进入页面。
窄屏状态下,横向空间不足,导航项可以折叠到菜单容器中,只保留品牌区域和菜单按钮。用户点击按钮后,菜单从隐藏状态变为展开状态,再次点击则恢复隐藏。
资料中的常见实现以 768px 作为桌面端和移动端的分界点。这个数值可以作为起点,但不应被当成必须遵守的标准。真正的断点应该根据导航项数量、品牌名称长度、字体大小和容器宽度来决定:只要横向排列开始拥挤,就应考虑提前进入折叠状态。
可以先记录一份简单的状态规划:
| 屏幕状态 | 导航链接 | 菜单按钮 | 主要布局 |
|---|---|---|---|
| 宽屏 | 默认显示 | 隐藏 | 品牌区域与链接横向排列 |
| 窄屏且菜单关闭 | 隐藏或折叠 | 显示 | 品牌区域与按钮保持一行 |
| 窄屏且菜单打开 | 显示 | 显示并反映打开状态 | 菜单在按钮下方展开 |
这一步的价值在于避免让 JavaScript 去“计算每个元素应该放在哪里”。元素怎么排列、何时显示,应该交给 CSS;脚本只需要改变一个明确的状态。
导航结构可以保持不变,桌面端和移动端共用同一组链接。这样既避免维护两份菜单,也能减少 WordPress 主题中菜单内容不同步的问题。
<nav class="site-navigation" aria-label="主导航">
<a class="site-navigation__brand" href="/">
网站名称
</a>
<button
class="site-navigation__toggle"
type="button"
aria-expanded="false"
aria-controls="site-navigation-menu"
>
<span class="site-navigation__toggle-line"></span>
<span class="site-navigation__toggle-line"></span>
<span class="site-navigation__toggle-line"></span>
<span class="screen-reader-text">打开菜单</span>
</button>
<div class="site-navigation__menu" id="site-navigation-menu">
<ul class="site-navigation__list">
<li><a href="/">首页</a></li>
<li><a href="/articles/">文章</a></li>
<li><a href="/about/">关于</a></li>
<li><a href="/contact/">联系</a></li>
</ul>
</div>
</nav>
这里有几个结构上的关键点:
nav 表示导航区域,便于浏览器和辅助技术理解页面结构。button,而不是用普通链接模拟按钮。aria-expanded 表示菜单当前是否展开。aria-controls 指向菜单容器的 id,明确按钮控制的对象。在 WordPress 主题中,导航链接可以由菜单函数输出,但外层容器、按钮和属性仍应保持稳定。这样脚本只需要绑定固定的按钮选择器,不必依赖具体菜单名称或链接数量。
桌面端样式可以先作为默认样式编写,再通过媒体查询调整窄屏布局。这样即使 JavaScript 暂时没有加载,桌面端也不会完全失去导航能力。
.site-navigation {
display: flex;
align-items: center;
justify-content: space-between;
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
.site-navigation__brand {
flex: 0 0 auto;
color: #222;
font-weight: 700;
text-decoration: none;
}
.site-navigation__menu {
margin-left: auto;
}
.site-navigation__list {
display: flex;
align-items: center;
gap: 1.25rem;
margin: 0;
padding: 0;
list-style: none;
}
.site-navigation__list a {
color: #333;
text-decoration: none;
}
.site-navigation__list a:hover,
.site-navigation__list a:focus-visible {
color: #1769aa;
}
.site-navigation__toggle {
display: none;
border: 0;
background: transparent;
cursor: pointer;
}
.site-navigation__toggle-line {
display: block;
width: 1.5rem;
height: 2px;
margin: 0.3rem 0;
background: #222;
transition: transform 180ms ease, opacity 180ms ease;
}
@media (max-width: 767px) {
.site-navigation {
position: relative;
flex-wrap: wrap;
}
.site-navigation__toggle {
display: block;
margin-left: auto;
}
.site-navigation__menu {
flex-basis: 100%;
display: none;
width: 100%;
margin-left: 0;
}
.site-navigation__menu.is-open {
display: block;
}
.site-navigation__list {
flex-direction: column;
align-items: stretch;
gap: 0;
padding-top: 0.75rem;
}
.site-navigation__list a {
display: block;
padding: 0.75rem 0;
}
.site-navigation__toggle[aria-expanded="true"]
.site-navigation__toggle-line:nth-child(1) {
transform: translateY(0.5rem) rotate(45deg);
}
.site-navigation__toggle[aria-expanded="true"]
.site-navigation__toggle-line:nth-child(2) {
opacity: 0;
}
.site-navigation__toggle[aria-expanded="true"]
.site-navigation__toggle-line:nth-child(3) {
transform: translateY(-0.5rem) rotate(-45deg);
}
}
这段样式体现了几个重要原则。
首先,宽屏下菜单默认显示,窄屏下菜单默认隐藏。媒体查询决定“当前屏幕应该采用哪种布局”,而不是由 JavaScript 逐个修改元素的宽度和位置。
其次,窄屏菜单使用 flex-basis: 100% 占据新的一行,品牌区域和按钮仍然保持在顶部。这样不需要额外复制一套移动菜单结构。
最后,按钮图标的变化也由 CSS 处理。JavaScript 只需要更新 aria-expanded,CSS 就能根据属性选择器将三条线变成关闭状态的图标。这种写法比脚本直接修改每条线的样式更容易维护。
脚本的职责可以收缩到四件事:
aria-expanded 和 CSS 类。const navigation = document.querySelector('.site-navigation');
const toggle = navigation?.querySelector('.site-navigation__toggle');
const menu = navigation?.querySelector('.site-navigation__menu');
if (toggle && menu) {
const setMenuState = (isOpen) => {
toggle.setAttribute('aria-expanded', String(isOpen));
menu.classList.toggle('is-open', isOpen);
const label = isOpen ? '关闭菜单' : '打开菜单';
const labelElement = toggle.querySelector('.screen-reader-text');
if (labelElement) {
labelElement.textContent = label;
}
};
toggle.addEventListener('click', () => {
const isOpen = toggle.getAttribute('aria-expanded') === 'true';
setMenuState(!isOpen);
});
menu.querySelectorAll('a').forEach((link) => {
link.addEventListener('click', () => {
setMenuState(false);
});
});
window.addEventListener('resize', () => {
if (window.innerWidth >= 768) {
setMenuState(false);
}
});
}
这里的 setMenuState() 是一个值得保留的小抽象。它把菜单状态的变化集中到一个位置,避免出现“按钮属性已经打开,但菜单类名没有同步”的问题。
点击导航链接后关闭菜单,是移动端比较自然的反馈。尤其当链接会跳转到新页面时,关闭动作不是必需的,但它可以避免在单页内容切换或锚点跳转场景下留下错误的视觉状态。
窗口尺寸变化也需要考虑。用户可能在浏览器开发者工具中拖动视口,或者在平板设备上改变窗口方向。当页面从窄屏切回宽屏时,脚本可以主动清除移动端的打开状态,防止之后重新回到窄屏时出现难以判断的状态残留。
以下做法通常会让维护成本快速上升:
display、width 或 position。aria-expanded。脚本当然可以读取窗口宽度,但它的目的应是处理状态边界,例如从移动端回到桌面端时关闭菜单,而不是替代媒体查询完成布局。
display: none 与 display: block 的切换本身无法平滑过渡。如果菜单只是简单展开,使用这种方式足够直接;如果确实需要动画,可以改为控制不透明度、最大高度或变换状态。
例如,可以使用可过渡的属性:
@media (max-width: 767px) {
.site-navigation__menu {
display: block;
max-height: 0;
overflow: hidden;
opacity: 0;
transition: max-height 220ms ease, opacity 180ms ease;
}
.site-navigation__menu.is-open {
max-height: 20rem;
opacity: 1;
}
}
这种写法不需要 JavaScript 计算菜单实际高度,但 max-height 需要留出足够空间。若菜单项未来会增加,固定值过小就可能导致内容被截断,因此应根据实际菜单规模调整,或者优先使用简单、可靠的显示切换。
对于不希望看到动画的用户,还应尊重系统的减少动态效果设置:
@media (prefers-reduced-motion: reduce) {
.site-navigation__menu,
.site-navigation__toggle-line {
transition: none;
}
}
动画是视觉反馈,不应影响菜单内容的可访问性和可操作性。
响应式导航出现问题时,不要一上来就修改 JavaScript。按照结构、样式、状态三个层次检查,定位会更快。
确认按钮、菜单容器和列表确实存在,aria-controls 的值与菜单的 id 完全一致。还要检查脚本执行时,导航元素是否已经出现在页面中。如果脚本放在页面头部执行,而导航尚未生成,选择器就可能取不到元素。
在 WordPress 主题中,还要确认菜单输出没有额外改变外层结构。尤其是主题模板更新后,原本绑定在固定类名上的脚本可能会失效。
使用浏览器开发者工具拖动视口,观察以下问题:
is-open 后菜单是否真的显示。overflow 截断。如果菜单一直不显示,优先查看 .site-navigation__menu.is-open 是否被更高优先级的规则覆盖,而不是马上重写脚本。
点击按钮时,检查开发者工具中的元素属性:
<button aria-expanded="true">
同时确认菜单容器是否出现:
<div class="site-navigation__menu is-open">
如果只有其中一个发生变化,说明状态同步函数没有覆盖全部需要更新的内容。若两者都变化但视觉没有改变,问题通常在 CSS 选择器、媒体查询或层叠顺序。
还应检查控制台是否出现元素为空、重复绑定事件或脚本语法错误。一个简单的导航开关不应依赖复杂的事件链,越早发现错误,越容易判断到底是结构、样式还是脚本出了问题。

规划这类导航时,可以用几个问题快速检查设计是否合理:
如果这些问题大多能得到肯定回答,这套导航结构通常就具备较好的扩展基础。后续即使加入当前页面高亮、二级菜单或主题自定义样式,也可以继续沿用“CSS 管布局,JavaScript 管状态”的边界,而不必把整个导航重写成一段难以拆分的脚本。
声明:原创文章请勿转载,如需转载请注明出处!