ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

纯HTML+CSS实现侧边伸缩导航栏:从布局到交互优化

2026/10/6 13:14:54 拓冰建站 浏览量
纯HTML+CSS实现侧边伸缩导航栏:从布局到交互优化 简介面向前端初学者的侧边伸缩导航栏实现代码包基于纯 HTML 与 CSS 完成适合在个人网站、后台管理界面或企业站中快速套用。资源包内含完整的页面结构、样式表及交互控制脚本通过复选框状态切换导航展开与收起并借助 transition 实现平滑动画同时包含移动端媒体查询思路帮助理解响应式侧边栏的常见写法。压缩包内共 9 个文件涵盖 html、css、js 等主要类型其中字体文件与图标配置用于丰富导航菜单的视觉表现整体仅 61KB轻量易部署。已有 6078 人学习下载适合想要掌握侧边栏布局、过渡动画及基础交互的前端学习者参考。代码结构简洁可直接打开预览或按需修改既能用作日常练习也可作为模板快速集成到实际项目中。1. 侧边伸缩导航栏纯HTMLCSS为什么够用自己搭过管理后台的朋友应该遇到过这种诉求页面左侧放一个菜单希望它能通过点击收起成窄条再点一次完整展开同时页面主体跟着自适应。大多数人的第一反应是引入UI框架但实际做一个可以长期维护的侧边伸缩导航栏时纯HTMLCSS组合就已经能覆盖大部分场景。下面这个项目要拆的就是这样一套代码不依赖Vue、React也不依赖任何第三方动画库只用原生HTML、CSS和一点可选的JavaScript就能实现侧边栏的展开、收起、子菜单二级展开和移动端抽屉效果。适合刚接触页面布局的新手用来练手也适合需要快速交付后台骨架页的从业者直接照搬。刚开始接触这个需求时我也以为折叠交互必须写JavaScript甚至考虑过给每个菜单项单独绑定事件。实际动手后发现整件事的核心只有两个点一个是控制侧边栏容器的宽度另一个是控制菜单文字在折叠时的显隐。只要把这两件事想清楚代码量其实非常小。下面我从结构、布局、交互到排错按我实际搭建的顺序逐步展开。每个环节都会给出完整代码和参数说明方便你直接复制运行再按需修改。2. 搭建侧边栏的HTML与CSS结构、布局、样式三块连做2.1 先从结构入手aside、nav、ul、li、a语义怎么分配普通布局里大多数人习惯写一堆div但实际开发时我会优先用语义标签因为后面的样式和脚本会好维护很多。外层侧边栏用aside而不是div表示这是一块辅助导航内部用nav包裹ul菜单项由li里的a承载这样搜索引擎和读屏软件都能比较清楚地识别页面结构。下面是这份资源的核心HTML骨架我把图标位先用单个汉字占位实际项目中换成SVG或字体图标即可!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title侧边伸缩导航栏/title link relstylesheet hrefsidebar.css /head body aside classsidebar idsidebar div classsidebar-header span classlogo侧栏/span button typebutton classcollapse-btn aria-label切换侧边栏 span classicon-btn≡/span /button /div nav ul classnav-list li a href# classnav-link span classnav-icon板/span span classnav-text仪表盘/span /a /li li a href# classnav-link span classnav-icon单/span span classnav-text订单管理/span /a /li li classhas-submenu button typebutton classnav-link submenu-toggle span classnav-icon设/span span classnav-text系统设置/span /button ul classsubmenu lia href# classnav-linkspan classnav-text用户管理/span/a/li lia href# classnav-linkspan classnav-text权限配置/span/a/li /ul /li /ul /nav /aside main classmain-content h1页面主体/h1 p侧边栏折叠时这里会自动占满剩余宽度。/p /main /body /html这里有几个位置建议你保留原样sidebar-header放logo和折叠按钮折叠按钮用button typebutton而不是a这样点击时不会触发默认跳转也不会在表单场景里意外提交.nav-icon是图标列折叠后保留.nav-text是文字列折叠时隐藏所以必须单独包一层span否则折叠后没法单独控制文字显隐。.has-submenu是带二级菜单的列表项它内部用了一个button来承载父级菜单而不是a href#。这样做的原因后面会讲主要是为了避免事件冒泡和hash跳转干扰折叠逻辑。二级菜单里的链接则照常使用a。整体这个结构即使去掉JavaScript在HTML语义层面也是完整的这也是我推荐这套写法的原因。2.2 CSS布局核心flex容器把侧边栏和主区域放一起有了结构之后第一件事是让侧边栏和主内容并排。这个场景我一般直接用 flex 布局不用浮动也不用绝对定位。flex 的优点是侧边栏宽度变化时主内容会自动伸缩不需要额外写JavaScript去计算剩余宽度。下面是布局部分的CSS* { margin: 0; padding: 0; box-sizing: border-box; } body { display: flex; min-height: 100vh; font-family: Microsoft YaHei, PingFang SC, sans-serif; } .sidebar { width: 240px; flex-shrink: 0; background: #1f2937; color: #d1d5db; transition: width 0.3s ease; overflow: hidden; white-space: nowrap; } .main-content { flex: 1; min-width: 0; background: #f8fafc; padding: 24px; }我来解释几个关键参数的用意。body设置display: flex后aside和main是两个横向排列的flex子项min-height: 100vh保证侧边栏高度撑满一屏而不是只包裹内容。.sidebar的宽度是240像素flex-shrink: 0防止窗口变窄时被挤压transition: width 0.3s ease是折叠动画的核心改变宽度时会产生平滑过渡。.main-content的flex: 1让它占据剩余空间但真正容易被忽略的是min-width: 0。默认情况下flex子项有一个min-width: auto如果主内容里有很长的连续文本或表格它会拒绝缩小导致页面出现横向滚动条。加上min-width: 0后主内容会被强制限制在剩余空间内这也是这个小细节容易踩的坑之一。侧边栏上的overflow: hidden和white-space: nowrap同样关键。折叠时宽度只剩64像素如果不禁止换行菜单文字会折行甚至露出容器之外overflow: hidden则负责把超出部分裁掉让文字像被吸进侧边栏一样视觉效果干净很多。常用参数我整理成一张表参数推荐值说明展开宽度240px适合大多数后台菜单可放完整文字折叠宽度64px足够放下一个图标或单个汉字过渡时长0.3s太快没反馈太慢影响操作文字大小14px菜单正文的常见字号侧栏底色#1f2937深灰蓝适合后台风格如果你后续要调整尺寸只需要改.sidebar的宽度值其他位置基本不用动。最常见的做法是先用240和64跑通再根据实际菜单长度微调。还有一点宽度变化只作用于aside内部内容不需要跟随布局计算所以动画过程中性能开销很低这在低端设备上也值得保留。2.3 视觉细节hover背景、圆角和二级菜单的状态布局跑通后再做菜单项的视觉反馈。导航栏的交互舒适度主要靠hover状态和菜单间距体现我常用的写法是把菜单项做成圆角卡片鼠标移上去时背景色轻微提亮与侧边栏底色形成层次。这里的样式同样适用于折叠状态因为折叠只是隐藏文字菜单项本身仍然是一行可以点击的区域.sidebar-header { display: flex; align-items: center; justify-content: space-between; height: 56px; padding: 0 12px; } .logo { font-size: 18px; font-weight: 600; color: #fff; white-space: nowrap; } .collapse-btn { width: 32px; height: 32px; border: none; border-radius: 6px; background: rgba(255, 255, 255, 0.08); color: #d1d5db; cursor: pointer; display: flex; align-items: center; justify-content: center; } .collapse-btn:hover { background: rgba(255, 255, 255, 0.16); } .nav-link { display: flex; align-items: center; gap: 10px; padding: 10px 14px; color: #d1d5db; text-decoration: none; border-radius: 8px; margin: 4px 8px; background: transparent; border: none; width: calc(100% - 16px); font-size: 14px; cursor: pointer; transition: background 0.2s ease, color 0.2s ease; } .nav-link:hover { background: rgba(255, 255, 255, 0.06); color: #fff; } .nav-icon { width: 24px; text-align: center; flex-shrink: 0; } .submenu { list-style: none; padding: 0; max-height: 0; overflow: hidden; transition: max-height 0.3s ease; } .has-submenu.open .submenu { max-height: 240px; }二级菜单的展开我选择用max-height而不是display: none因为max-height可以从0过渡到一个固定像素值产生平滑展开动画。常见做法是把上限设置成240像素三级以上的复杂菜单建议嵌套结构里也遵循这个思路。.nav-link同时用于父级按钮和普通链接所以样式里既要兼容a的text-decoration: none又要兼容button默认的border: none和background: transparent否则父级菜单按钮会保留浏览器默认外观观感非常割裂。另外注意.nav-link中gap: 10px是图标和文字的间距。折叠时如果只用display: none隐藏文字这个间距不会残留但如果用opacity或visibility隐藏文字文字盒子仍占空间间距就会留下来这一点会在后面的排查章节展开。到这里一个不会折叠、但结构完整的侧边栏已经跑起来了下一步给它接上真实的折叠交互。3. 折叠交互机制JS类名方案与纯CSS方案对照实现3.1 最实用的方式点击按钮切换一个类名折叠交互的本质是让侧边栏容器在“展开”和“收起”两种状态之间切换。我通常用JavaScript给aside切换一个collapsed类所有折叠样式集中写在类名下面。理由很简单状态由类名表达后续如果需要记录用户偏好、路由联动或者动画钩子都围绕这个类名扩展就行不用改动HTML结构。先看JavaScript部分。这段代码放在HTML底部确保DOM已经加载完成再绑定事件const sidebar document.getElementById(sidebar); const collapseBtn document.querySelector(.collapse-btn); const submenuToggles document.querySelectorAll(.submenu-toggle); collapseBtn.addEventListener(click, () { const isCollapsed sidebar.classList.toggle(collapsed); collapseBtn.setAttribute(aria-expanded, String(!isCollapsed)); }); submenuToggles.forEach(toggleBtn { toggleBtn.addEventListener(click, (e) { e.stopPropagation(); const parentLi toggleBtn.closest(li.has-submenu); parentLi.classList.toggle(open); }); });classList.toggle(collapsed)的返回值很关键如果当前没有collapsed执行后返回true说明现在是折叠状态如果有collapsed执行后返回false说明现在展开了。aria-expanded属性需要字符串类型的true或false所以代码里用String(!isCollapsed)取反转换表达的是“侧边栏是否处于展开状态”。子菜单按钮的监听我加了e.stopPropagation()这是为了避免点击父级菜单时事件冒泡到外层容器触发不必要的折叠逻辑。如果你后续把整块侧边栏的点击事件委托到aside上这个停止冒泡会更重要。closest(li.has-submenu)用于从当前按钮往上找到最近的父级列表项再给它切换open类这样HTML结构里不需要为每个子菜单单独写ID可维护性好很多。对应折叠部分的CSS是这样处理文字显隐的.sidebar.collapsed { width: 64px; } .sidebar.collapsed .nav-text, .sidebar.collapsed .logo { display: none; } .sidebar.collapsed .submenu { max-height: 0; overflow: hidden; padding: 0; }这里我直接使用display: none来隐藏文字和logo因为它们在折叠时完全没有存在意义。display: none是即时生效的不会参与过渡动画所以主视觉上你看到的是侧边栏宽度从240平滑收缩到64而文字在收缩过程中直接被裁掉效果并不突兀。不推荐在这个场景里用opacity: 0因为它会让文字在宽度收缩的瞬间还在flex布局里占位置反而造成最后几像素的跳动。折叠后宽度64像素刚好容纳.nav-icon加上左右padding。如果你实际项目里没有图标只有纯文字菜单建议把折叠宽度调大到80像素左右否则菜单项里就只剩一块空白区域。点击折叠按钮时也可以给按钮加一个旋转反馈让用户明确知道当前状态例如给.collapse-btn .icon-btn加上transform: rotate(180deg)配合同样0.3秒的过渡整体手感会好很多。如果希望同时间只展开一个子菜单可以再补一小段逻辑在展开当前子菜单前先把其它所有.has-submenu.open移除。这个功能在后台系统里非常常见但属于增强需求不写在基础代码里也能正常工作。3.2 可以完全不用JScheckbox加label也能折叠如果你的项目希望零JavaScript依赖或者只是想快速做一个静态原型还有一个更省事的方案用input typecheckbox加label实现状态切换。原理是label点击后会切换checkbox的选中状态CSS通过:checked伪类和兄弟选择器控制侧边栏宽度。实现这个方案HTML结构需要调整一下。checkbox必须放在侧边栏之前并且和侧边栏是同一层级的兄弟节点否则后面的~选择器无法命中input typecheckbox idnavToggle classsidebar-ck checked label fornavToggle classcollapse-btn≡ 切换/label aside classsidebar !-- 内部结构保持和之前一致 -- /asideCSS部分变成这样.sidebar-ck { position: absolute; left: -9999px; } .sidebar-ck:checked ~ .sidebar { width: 240px; } .sidebar-ck:not(:checked) ~ .sidebar { width: 64px; } .sidebar-ck:not(:checked) ~ .sidebar .nav-text, .sidebar-ck:not(:checked) ~ .sidebar .logo { display: none; }checkbox初始带有checked属性所以页面加载时侧边栏默认展开。用户点击label后checkbox从未选中变成选中或者从选中变成未选中CSS立即把对应宽度套用上去。label的样式可以完全复用之前.collapse-btn的样式视觉上没有差别。不过这个方案有它的边界。第一状态不持久刷新页面后永远回到初始状态没法记住用户最后一次的折叠选择。第二二级菜单的展开仍然需要额外处理除非为每个子菜单再配一对checkbox和label结构会变得很啰嗦。第三移动端如果要做抽屉遮罩光靠checkbox状态控制遮罩的显隐也能实现但逻辑会绕。所以我一般只在演示页面或纯静态单页场景里用它正式后台项目还是走JS类名方案更稳妥。3.3 两个方案怎么选JS和纯CSS的边界为了让你快速决策我把两种方案的实际表现整理成表格。这里不比较谁更“高级”只比较在真实项目里谁会让你少加班能力项JS类名方案checkbox纯CSS方案折叠状态持久化可配合localStorage不支持二级菜单扩展灵活类名控制结构繁琐移动端抽屉容易加遮罩和关闭逻辑需要额外结合checkbox键盘可访问性按钮原生支持label和checkbox也支持代码维护量中等小但扩展受限适合场景后台管理系统、长期迭代项目静态页、快速原型、纯演示从我的经验看只要这个侧边栏会跟着项目长期迭代我都会直接选JS类名方案。不是因为纯CSS方案不行而是后台项目后期大概率会加路由高亮、根据权限动态生成菜单、记住折叠状态这些需求这些最终都要落在JavaScript上。与其做到一半再重构不如一开始就用类名方案把折叠看成“给容器换个状态”这件事。4. 侧边栏排查手册五个高频问题与解决路径4.1 点击折叠按钮没有反应现象点击按钮后侧边栏纹丝不动或者页面直接刷新一次控制台还可能报错。原因最常见的是script放在head里且没包DOMContentLoaded导致页面还没渲染完就执行查找元素拿到null。另一种情况是把折叠按钮写在了form里又没设置typebutton按钮默认的提交行为把表单提交了。解决把script尽量放在/body前或者统一包一层事件监听。按钮务必写成button typebutton这能排除掉90%的奇怪问题。结构上检查idsidebar是否存在以及类名大小写是否一致。我给script套上DOMContentLoaded后这种问题基本没有再出现过。4.2 文字不跟随收起或者动画很生硬现象侧边栏宽度确实从240变到64但菜单文字被挤成竖排甚至突出侧边栏之外或者宽度瞬间跳变完全没有过渡效果。原因父容器没加overflow: hidden文字在宽度变窄时自动换行撑破容器transition属性写在了错误的元素上另外折叠时只隐藏了部分文字比如logo还在导致视觉上有一块内容突然消失。解决在.sidebar上同时设置overflow: hidden和white-space: nowrap这两行缺一不可。给aside本身加transition: width 0.3s ease而不是给文字加transition。折叠状态用display: none统一隐藏logo和所有.nav-text不要试图用width或height去压缩文字那是纯走弯路。4.3 页面出现横向滚动条现象折叠和展开切换时页面底部的横向滚动条出现又消失或者主内容比视口宽出一截。原因flex布局下.main-content的默认min-width: auto会参考内容的最小宽度。如果里面有一张很宽的表格、一段不换行的代码主内容就不会收缩把总宽度撑破。另一个原因是侧边栏自身缺少flex-shrink: 0窗口变窄时侧边栏被压缩但内部内容没有跟着隐藏。解决给.main-content设置min-width: 0这是最关键的修复。再给.sidebar设置flex-shrink: 0。如果主内容里确实有需要横向滚动的大表格就应该让表格容器overflow-x: auto不要让它的宽度影响整条页面的布局。4.4 子菜单和父级折叠互相干扰现象点击“系统设置”想要展开子菜单结果外层侧边栏也被触发了折叠或者点击子菜单里的链接后整个菜单收起页面回到顶部。原因事件冒泡。如果侧边栏根元素绑定了全局点击事件点击子菜单按钮时事件会一层层冒泡上去触发了外层逻辑。另一个常见原因是父级菜单用了a href#点击后hash变化浏览器跳回顶部看起来就像导航栏失控。解决子菜单切换事件里加e.stopPropagation()阻止事件继续向外传递。父级菜单用button而不是a从根源上避免hash变化。如果还是要用a就把href#去掉改用CSS cursor或按钮交互。我通常把父级菜单做成按钮子菜单里的具体链接保持a两边的职责非常清楚。4.5 折叠状态刷新后丢失现象用户把菜单收起来刷新页面后又变回展开状态每次都要重新操作一次。原因折叠状态只存在于JS内存里没有持久化。后台系统里用户可能每天反复折叠同一个菜单这个损失体验的问题看似小其实很影响日常使用。解决用localStorage记录状态。初始化时读取一次点击折叠按钮时写入一次代码量很小在下一章的进阶部分我会给出具体实现。5. 进阶应用移动端抽屉、ARIA状态和状态记忆5.1 移动端抽屉侧边栏在窄屏时变成悬浮层桌面端的64像素折叠宽度在手机屏幕上依然显得占地正确做法是让侧边栏隐藏起来点击菜单按钮时以抽屉形式从左侧滑出。基于JS类名方案可以在768像素以下用media query覆盖默认布局media (max-width: 768px) { body { display: block; } .sidebar { position: fixed; left: 0; top: 0; bottom: 0; z-index: 100; width: 240px !important; transform: translateX(-100%); transition: transform 0.3s ease; } .sidebar.open { transform: translateX(0); } }这里我把手机端的侧边栏固定在视口左侧默认用translateX(-100%)推到屏幕外当JS给aside添加open类时侧边栏滑入。桌面端的collapsed类在移动端不生效所以用width: 240px !important强制保持展开宽度。与此同时桌面端的折叠按钮在移动端可以切换open类而不是切换collapsed具体可以在按钮事件里根据窗口宽度判断走哪套逻辑。5.2 给折叠按钮补上ARIA状态和键盘焦点折叠按钮在桌面端切换的是collapsed在移动端切换的是open但从可访问性角度看按钮都应该准确表达当前侧边栏是否展开。aria-expandedtrue表示侧边栏可见false表示折叠状态。这条属性在基础代码里已经通过JS动态写入这里单独拎出来是提醒你如果侧边栏折叠后隐藏了所有文字读屏软件用户无法扫读菜单最好在折叠状态下给每个.nav-link或对应的按钮补一个aria-label例如“仪表盘当前折叠”。单纯靠CSSdisplay: none隐藏文字虽然会被读屏忽略但也意味着关键词信息一同丢失所以能加title或aria-label时不要省。5.3 用localStorage记住用户选择最后加一个我每次都会做的状态恢复。折叠偏好是后台用户很在意的细节存储和读取都极其简单const sidebar document.getElementById(sidebar); const collapseBtn document.querySelector(.collapse-btn); if (localStorage.getItem(sidebar-collapsed) 1) { sidebar.classList.add(collapsed); } collapseBtn.addEventListener(click, () { const collapsed sidebar.classList.toggle(collapsed); collapseBtn.setAttribute(aria-expanded, String(!collapsed)); localStorage.setItem(sidebar-collapsed, collapsed ? 1 : 0); });这段代码唯一的注意点是初始化和绑定顺序必须在读取状态之前拿到sidebar元素在按钮绑定事件之前完成初始类名设置否则用户一刷新就会看到侧边栏莫名从折叠变成展开。平时我在多项目里也遇到过localStorage被清理导致的短暂闪烁更稳妥的做法是给body加一个启动类等状态读取完成后再移除不过对于普通后台系统上面这段已经足够稳定。最后再分享一次我的固定检查流程。每次写完侧边栏我都会把浏览器宽度拖到375像素走一遍移动端抽屉再用Tab键从折叠按钮一路按到子菜单最后一项确认焦点落在可见元素上最后刷新页面看折叠状态是否被还原成用户上次的选择。这个流程不复杂但覆盖了我踩过的大多数侧边栏翻车场景。有一次我偷懒没做移动端检查结果用户反馈侧边栏在手机上半悬浮半占位把主内容顶出了屏幕后来才把min-width: 0和移动端position: fixed一起补齐。从那以后我每次改侧边栏结构都会强制跑一遍这个流程基本没再犯过同类错误。希望这篇拆解能帮到你少绕几个弯。本文还有配套的精品资源点击获取