纯 CSS 汉堡菜单必须用 <input type="checkbox">,因其 :checked 伪类是唯一无需 JS 即可响应点击并切换样式的机制;配合隐藏复选框、兄弟选择器、正确 transform-origin 及层叠上下文处理,实现图标旋转与菜单显隐。

为什么纯 CSS 汉堡菜单要用 <input type="checkbox">
因为 CSS 本身没有事件监听能力,:checked 是唯一能响应用户点击并触发样式切换的伪类。用 <input type="checkbox"> 隐藏后,它就成了一个“状态开关”——点一下变选中,再点一下取消,CSS 就能据此控制图标旋转和菜单显隐。
别用 <button> 或 <div> + onclick,那必须配 JS;也别用 <input type="radio">,它无法“关掉”,菜单展开后没法收回去。
-
<input id="menu-toggle" type="checkbox">必须放在 DOM 前置位置(比如<body>开头),否则兄弟选择器~会失效 - 对应
<label for="menu-toggle">要紧挨着它,或用+相邻选择器控制图标动画 -
opacity: 0; position: absolute;隐藏复选框,但不能写display: none,否则状态不可读
transform-origin 写错会导致汉堡图标变形错位
三道横线转成 × 的动画,本质是三根 <span> 分别旋转:中间线转 45°,上面那根转 0°(与之中重合),下面那根转 90°(垂直交叉)。但所有旋转都依赖 transform-origin 的基准点。
如果没设或设成默认的 center,线条会绕自己中心转,结果是“飘在空中”,而不是从左端“翻折”过去。正确做法是统一设为 left center:
立即学习“前端免费学习笔记(深入)”;
span {
transform-origin: left center;
}
#menu-toggle:checked + .menu-btn > span {
transform: rotate(45deg);
}
#menu-toggle:checked + .menu-btn > span::before {
transform: rotate(0deg);
}
#menu-toggle:checked + .menu-btn > span::after {
transform: rotate(90deg);
}
注意:::before 和 ::after 的 transform-origin 默认继承父元素,所以也要显式声明,否则安卓 WebView 上容易错位。
菜单弹出后被遮挡?大概率是 z-index 失效了
不是数值不够大,而是父容器创建了新的层叠上下文(stacking context),把菜单“锁”在局部层级里。常见触发条件:transform、opacity < 1、filter、will-change —— 只要 <header> 或 <nav> 用了其中任意一个,菜单就可能被盖住。
- 用 DevTools 查看菜单元素的 computed
z-index,如果是auto,说明没生效 - 把菜单容器(
.menu-box)动态挂到<body>下,绕过父级限制 - 确保
position: fixed的菜单有top: 0; left: 0; width: 100vw; height: 100vh;,不用%,避免继承缩放干扰 - 加
will-change: transform;在动画期间,但别写在默认状态里
移动端点击无响应?检查触控热区和 pointer-events
很多“点了没反应”其实是按钮根本没被点中。iOS 要求最小触控区域 ≥ 44×44pt,安卓 WebView 对 opacity: 0 的 <input> 有时会拦截点击。
-
<label>必须设display: block; width: 44px; height: 44px;,哪怕图标只有 24px - 隐藏的
<input>加pointer-events: none;,否则某些安卓机型会吞掉事件 - 别用
touch-action: none,它会禁用所有手势;改用touch-action: manipulation;提升响应优先级 - 测试时用真机 Safari / Chrome DevTools 的 device toolbar,模拟器常漏掉 touch 事件细节
真正难调的不是怎么动起来,而是让每台手机都稳稳点中、每次展开都贴边、每个状态切换都无卡顿——这些细节堆在一起,才是移动端汉堡菜单落地的最后一公里。


















