☰
CSS侧边伸缩导航栏实战:从布局到响应式优化的完整指南
2026/10/6 3:31:48 网站建设 项目流程

简介:这是一份面向网页前端初学者及日常需要搭建后台界面的人员的侧边伸缩导航栏示例,讲解如何用最基本的HTML与CSS实现可折叠侧边栏。资源从容器级导航结构讲起,包含链接、子菜单、切换按钮等元素,并用到#toggle:checked状态选择器控制面板宽度、transition过渡实现平滑伸缩,再配合媒体查询处理小屏幕下的隐藏与呼出;悬停、聚焦等伪类也被用来增强操作反馈。包内共9个文件,压缩包仅61KB,以两个html页面、两个css样式表和少量js、图标字体、效果图构成;其中css是核心实现,json/ttf用于管理iconfont图标,png可直观对照最终视觉效果。资源已有6078人浏览学习,适合想快速拿到一份可直接运行代码并从中理解CSS交互效果的读者。通过该资源既能直接部署到个人项目中,也可作为学习CSS伪类选择器和响应式布局的辅助样例,或在此基础上扩展动画效果和移动端手势。

1. 侧边伸缩导航栏:会动的窄条背后是三个问题

第一次做 html+css 侧边伸缩导航栏的人,通常以为这只是一个“宽度从 220px 变到 64px”的动画。实际落地你会发现,侧边栏一缩,内容区要跟着动;内容区跟着动,遮罩层要不要出现又成了新问题;再叠上子菜单展开,动画就完全不是看起来那么一回事。这篇文章按“结构—交互—联动—排错—进阶”的顺序,把一个侧边导航最常见的实现路径完整过一遍。适合刚写完静态页面、想给后台页面加交互的新手,也适合已经在项目里写过一版、但总觉得动画别扭的开发者照着排查。

2. 先立骨架:aside + main 两列布局与四个必调过渡参数

侧边伸缩导航栏的静态骨架,核心就是两个元素:左侧的 aside 负责导航,右侧的 main 放内容。伸缩动画能不能做顺,一半取决于 CSS 的参数选得对不对,另一半取决于你选的是哪种布局模式。所以这一章先花时间把布局打牢,后面加 JS 才不会返工。

2.1 最小 HTML 结构:aside、main 与一个按钮的职责划分

常见做法是给整个页面套一个layout容器,里面放 aside 和 main 两个兄弟节点。按钮放在 main 的顶部,而不是放在侧边栏内部。原因很直接:侧边栏折叠后只剩 64px,里面放按钮会被挤得很难看;而且按钮放在内容区,折叠和展开的触发位置始终固定,不会因为侧边栏宽度变化而跟着跑。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>侧边伸缩导航栏</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="layout"> <aside class="sidebar" id="sidebar"> <div class="sidebar-logo">Logo</div> <nav class="sidebar-nav"> <ul> <li><a href="#">首页</a></li> <li><a href="#">订单管理</a></li> <li><a href="#">用户列表</a></li> <li><a href="#">系统设置</a></li> </ul> </nav> <div class="sidebar-footer">v1.0.0</div> </aside> <main class="content" id="content"> <button class="collapse-btn" id="collapseBtn" aria-expanded="true"> <span class="btn-text-collapse">收起侧边栏</span> <span class="btn-text-expand">展开侧边栏</span> </button> <div class="content-body"> <h1>内容区</h1> <p>这里放页面主体内容。</p> </div> </main> </div> <script src="script.js"></script> </body> </html>

这段结构把职责分得很清楚:aside 只负责导航,main 只负责主体内容,按钮是 main 里的一个控制入口。按钮里放了两段文字,默认显示“收起侧边栏”,折叠后通过 CSS 类切换显示“展开侧边栏”,这个方案比在 JS 里改textContent更省事,状态也完全由 CSS 驱动。

要说明的是,有些纯 CSS 方案会用隐藏的 checkbox 加 label 实现伸缩,代码确实更短,但状态读取不方便,也不利于后面接 localStorage 记忆功能,所以这里不采用。记住这个选择,后面第 3 章会解释为什么 class 切换是更稳的做法。

2.2 折叠的四个必调参数:width、transition、overflow、white-space

骨架有了,接下来是让侧边栏收得干净的四个 CSS 参数。很多人只写了width和transition,结果折叠后文字从右侧漏出来,或者一收缩就把整个页面撑出横向滚动条,问题就出在后面两个参数上。

.layout { display: flex; min-height: 100vh; } .sidebar { width: 220px; flex-shrink: 0; overflow: hidden; background: #1e293b; color: #e2e8f0; transition: width 300ms cubic-bezier(0.4, 0, 0.2, 1); } .sidebar.collapsed { width: 64px; } .sidebar-nav a { display: block; color: inherit; text-decoration: none; white-space: nowrap; line-height: 44px; padding: 0 20px; }

transition必须写在侧边栏的默认状态里,而不是写在.collapsed里面。这是个很容易踩的细节:如果把transition放到折叠态,那么点击按钮让侧边栏收回时是有动画的,再次展开时却是瞬变,因为展开时元素回到了没有transition的默认状态,动画自然就丢了。

overflow: hidden负责挡住折叠后超出 64px 的菜单文字,white-space: nowrap则是从源头防止文字换行。两个参数缺一个,窄条状态下都会出问题。flex-shrink: 0是 flex 布局里的附加保险,它防止容器宽度紧张时侧边栏被压缩成一个非预期的宽度,否则伸缩动画的对齐基准就乱了。

下面这张表整理了这几个参数的作用,改代码时可以对着看:

参数值作用
width220px / 64px展开态与折叠态的宽度基准
transition300ms cubic-bezier(0.4, 0, 0.2, 1)宽度变化的过渡动画
overflowhidden折叠时隐藏溢出的文字
white-spacenowrap禁止菜单文字换行撑宽
flex-shrink0防止 flex 容器压缩侧边栏宽度

cubic-bezier(0.4, 0, 0.2, 1)是 Material Design 里常用的标准曲线,前段快、后段慢,比ease或linear跟手。嫌调参麻烦可以直接抄这个值,它在宽度变化这种场景下基本不会翻车。

2.3 固定定位还是 flex:两种常见布局模式的取舍

写到这里,需要做一个分叉决策:侧边栏用position: fixed还是外层容器用display: flex。两者都能实现伸缩,但内容区的联动方式完全不同,选错了后面要返工。

fixed 方案的典型写法是这样:

.sidebar { position: fixed; top: 0; left: 0; width: 220px; height: 100vh; transition: width 300ms cubic-bezier(0.4, 0, 0.2, 1); } .content { margin-left: 220px; transition: margin-left 300ms cubic-bezier(0.4, 0, 0.2, 1); } .sidebar.collapsed { width: 64px; } .sidebar.collapsed ~ .content { margin-left: 64px; }

fixed 方案里,侧边栏脱离文档流,内容区必须手动用margin-left让出位置。为了让内容区移动和侧边栏收缩保持同步,margin-left的过渡时长和曲线必须跟侧边栏的width过渡完全一致,否则视觉上会出现侧边栏已经收好、内容区还在慢慢挪的错位感。

flex 方案则简单一些:

.layout { display: flex; min-height: 100vh; } .sidebar { width: 220px; flex-shrink: 0; transition: width 300ms cubic-bezier(0.4, 0, 0.2, 1); } .content { flex: 1; min-width: 0; }

flex 方案里,内容区的宽度是浏览器根据剩余空间自动算出来的。侧边栏从 220px 缩到 64px,内容区自动变宽,完全不需要额外写联动代码。代价是侧边栏和内容区同处一个滚动上下文,页面很长时内容区没法独立滚动。

两种布局模式各有适用场景,选型时可以参考这张表:

对比项fixed 方案flex 方案
内容区联动margin-left 手动同步自动补齐剩余宽度
内容区滚动可独立滚动跟随页面整体滚动
实现难度中,选择器要写对低,结构简单
适用场景长表格、需要内容独立滚动的后台典型后台管理系统

我一般会优先用 flex 方案,就是因为内容区联动是自动的,少一个margin-left就少一对需要同步的过渡参数。但如果你要做的页面主体是一个需要独立滚动的大表格,fixed 能让表头固定、内容滚动,这个优势是 flex 模拟不出来的。两种方案都是常见做法,关键还是先想清楚页面滚动结构再动手。

3. 让伸缩真正动起来:class 切换与 transitionend 监听

静态骨架搭好后,接下来就是控制逻辑。侧边栏伸缩的 JS 非常简单,但正因为简单,很多人容易写出一堆问题:直接在 JS 里改style.width、用定时器模拟动画、或者把状态分散在 DOM 属性里。这章把控制部分的正确写法拆开讲清楚。

3.1 不要裸改 width:class 作为唯一状态源

直接在 JS 里写sidebar.style.width = '64px'当然能实现收缩,但这不是一个可维护的做法。因为状态散落在两处:CSS 里的width: 220px管展开态,JS 里的 inline style 管折叠态。以后想加一个“记住用户折叠偏好”的功能,或者想把展开态从 220px 调整到 240px,你就要同时改 CSS 和 JS,一个漏改就是线上事故。

更好的做法是让 CSS 管视觉、JS 只管状态。侧边栏加一个collapsed类,所有折叠相关的样式都由这个类驱动。JS 只负责把类加上或移除,完全不碰具体的像素值。这样侧边栏的展开态、折叠态以及它们各自的过渡动画,全都在 CSS 里有一个统一的定义,这叫“单一状态源”。

类切换的另一个好处是后面接动画库、状态管理、浏览器持久化都方便。第 6 章会用 localStorage 记住折叠状态,到那时你就能体会到,如果一开始是裸改style.width,恢复状态的代码会难写到什么程度。

3.2 最小 JS 代码:toggle 按钮与 aria-expanded 状态同步

控制侧边栏收缩的最小 JS 代码,其实只需要一个事件监听和一行classList.toggle。

const sidebar = document.getElementById('sidebar'); const collapseBtn = document.getElementById('collapseBtn'); collapseBtn.addEventListener('click', () => { const collapsed = sidebar.classList.toggle('collapsed'); collapseBtn.setAttribute('aria-expanded', String(!collapsed)); });

classList.toggle('collapsed')有一个返回值:如果类被加上,返回true;如果类被移除,返回false。这个返回值正好可以拿来同步aria-expanded,不需要再单独用contains判断一次。注意setAttribute第二个参数必须是字符串,所以要用String(!collapsed)把布尔值转一下。

按钮文字跟着状态切换,不用 JS 改textContent,用 CSS 兄弟选择器更干净:

.sidebar.collapsed ~ .content .btn-text-collapse { display: none; } .sidebar.collapsed ~ .content .btn-text-expand { display: inline; } .btn-text-expand { display: none; }

默认情况下只显示“收起侧边栏”,等.sidebar有collapsed类时,按钮里的“收起”隐藏、“展开”出现。这利用了第 2.3 节里 aside 和 main 的兄弟关系,选择器写起来也比较直观。

如果还想照顾键盘用户,可以加一个 Escape 键收起的逻辑:

document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { sidebar.classList.add('collapsed'); collapseBtn.setAttribute('aria-expanded', 'false'); } });

这里用add而不是toggle,是为了保证即使侧边栏已经处于折叠状态,按 Escape 也不会把状态反转为展开。细节很小,但能避免键盘用户按错键时的困惑。

3.3 子菜单展开:max-height 过渡的边界与溢出处理

后台侧边导航几乎都有二级菜单,菜单展开收起的效果,CSS 里最常用的手段是过渡max-height。这个属性是少数可以过渡的“非确定性高度”之一。普通height: auto无法触发过渡,但max-height可以从 0 过渡到一个足够大的值,视觉上就像高度在变化。

<ul> <li> <a href="#">商品管理</a> <ul class="submenu"> <li><a href="#">商品列表</a></li> <li><a href="#">分类管理</a></li> </ul> </li> </ul>
.submenu { max-height: 0; overflow: hidden; transition: max-height 300ms ease; } .submenu.open { max-height: 300px; }
const nav = document.querySelector('.sidebar-nav'); nav.addEventListener('click', (e) => { const link = e.target.closest('a'); if (!link) return; const submenu = link.parentElement.querySelector('.submenu'); if (submenu) { submenu.classList.toggle('open'); } });

max-height: 300px这个值有个讲究:它要比子菜单内容实际渲染出来的高度大。内容只有两行时,300px 已经绰绰有余;但如果某个菜单有七八个子项,300px 不够就会直接把内容截断。

提示:max-height设得越大,同样300ms内过渡的速度感知就越快。如果设成1000px,子菜单会显得比侧边栏收缩快很多,观感上会有点“冲”。落地时先量一下子菜单实际高度,再在这个值上加 30% 的余量。

还有一个边界情况需要提前处理:侧边栏处于折叠状态时,子菜单必须隐藏。否则侧边栏收成 64px 窄条后,二级菜单的文字会从侧边栏底部挤出来。加一条:

.sidebar.collapsed .submenu { max-height: 0; }

这条规则配合第 5.4 节会再展开讲一下,因为折叠侧边栏和展开子菜单这两个动画同时触发时,会出现两个过渡“打架”的观感问题,那是另一个要单独处理的坑。

4. 内容区联动与遮罩层:margin-left、flex 弹性与点击穿透

侧边栏本身能收缩只是第一步。接下来才是最影响实际体验的部分:内容区怎么跟着动,以及必要的时候怎么用遮罩层盖住内容区。这一章把两套联动方案和遮罩层的细节讲透。

4.1 联动方案一:在 fixed 中同步过渡 margin-left

第 2.3 节里 fixed 方案的完整版,就在这里接上。侧边栏 fixed 之后会脱离文档流,内容区如果不设margin-left,就会直接钻进侧边栏底下。所以内容区必须让出侧边栏的宽度,而且这个让位动作也要有过渡动画,否则侧边栏收的时候内容区“啪”地一下跳过去,观感非常生硬。

.sidebar { position: fixed; top: 0; left: 0; width: 220px; transition: width 300ms cubic-bezier(0.4, 0, 0.2, 1); } .content { margin-left: 220px; transition: margin-left 300ms cubic-bezier(0.4, 0, 0.2, 1); } .sidebar.collapsed { width: 64px; } .sidebar.collapsed ~ .content { margin-left: 64px; }

这里最关键的一行是.sidebar.collapsed ~ .content。波浪号是兄弟选择器,它选中的是sidebar后面同级的.content元素。因为 HTML 里 aside 和 main 是同级兄弟节点,这个选择器才能生效。如果中间套了一层父容器,选择器就要改成.sidebar.collapsed .content或手动在 JS 里控制内容区的类,写法会麻烦不少。

两个transition的时长和缓动曲线必须完全一致。如果侧边栏是 300ms、内容区是 250ms,收的时候侧边栏先到位、内容区后到位,中间会有明显的一段错位。实际项目里看到侧边栏动画“脱节”的,十有八九是两个时长没对齐。

注意:这里的transition-delay要留 0。有人为了让内容区“慢半拍”好看,给margin-left加了0.1s的延迟,结果侧边栏收完内容区才开始动,视觉上更难受。除非你刻意做分层效果,否则联动动画的过渡参数必须保持同步。

4.2 联动方案二:flex 方案的内容区自动补偿

flex 方案里,内容区不需要写任何 margin。因为 flex 布局的核心机制就是剩余空间分配,侧边栏宽度一变,内容区自动补齐。这也是我前面说优先用 flex 的原因,少一套需要同步的参数,就少一个出错源。

.layout { display: flex; height: 100vh; } .sidebar { width: 220px; flex-shrink: 0; transition: width 300ms cubic-bezier(0.4, 0, 0.2, 1); } .content { flex: 1; min-width: 0; }

.content上的min-width: 0是一个很容易被漏掉的细节。flex 子项的min-width默认值是auto,含义是“不允许被压缩到小于内容的固有宽度”。如果内容区里有一个很宽的表格或一长串不换行的文字,flex: 1会让内容区被内容撑开,导致整个页面出现横向滚动条,侧边栏的宽度也会跟着被挤压。

加了min-width: 0之后,内容区允许被压缩到任意宽度,横向滚动条的问题就消失了。这个属性对新手来说有点反直觉,因为它像是在“破坏” flex 的自动分配,但实际上它才是让内容区在极端内容条件下仍然保持正确布局的关键。

flex 方案下侧边栏的宽度变化,浏览器会自动重排内容区,动画同样是基于transition的,因为浏览器在过渡宽度时,内容区的几何位置是逐帧跟着变动的。不需要 JS 参与任何计算,你只需要确保侧边栏的宽度变化发生在有transition的属性上。

4.3 遮罩层:什么时候必须加、怎么做到不挡点击

遮罩层不是所有侧边导航都需要。桌面宽屏的折叠式侧边栏,内容区本来就看得见,加了遮罩反而挡操作。但手机端的抽屉式侧边栏就必须有遮罩:侧边栏从左侧滑出时,内容区需要有层半透明背景盖住,用户点遮罩就能收起导航,这比强制点关闭按钮更符合移动端的交互习惯。

遮罩层的 HTML 可以动态创建,也可以直接写在页面里:

<div class="sidebar-mask" id="sidebarMask"></div>
.sidebar-mask { position: fixed; inset: 0; z-index: 999; background: rgba(15, 23, 42, 0.5); opacity: 0; visibility: hidden; transition: opacity 300ms ease, visibility 300ms ease; } .sidebar-mask.active { opacity: 1; visibility: visible; }
const mask = document.getElementById('sidebarMask'); function openSidebar() { sidebar.classList.remove('collapsed'); mask.classList.add('active'); } function closeSidebar() { sidebar.classList.add('collapsed'); mask.classList.remove('active'); } mask.addEventListener('click', closeSidebar);

遮罩层的显示隐藏,要同时用opacity和visibility两个属性。只用opacity的问题在于:透明度为 0 的遮罩层仍然占据页面并拦截点击,用户明明看到内容区已经可以点了,但点下去没有反应,这就是“点击穿透”的变种。visibility: hidden本身是不响应点击的,所以把它和opacity一起过渡,既能做淡入淡出,又能在隐藏后彻底让出点击区域。

z-index: 999这个值要保证比侧边栏大、比弹窗小。如果项目里有 Modal 组件,通常 Modal 的 z-index 是 1000 或更高,遮罩层比 Modal 低就对了,否则弹窗会被遮罩盖住。

遮罩层在宽屏折叠模式下不需要显示,只在移动端抽屉模式下才出现。这个模式切换的逻辑放到第 6 章,用matchMedia断点统一处理,避免在 CSS 和 JS 里到处散落判断。

5. 侧边导航栏常见问题排查:五个高频翻车点与修复

侧边伸缩导航栏代码量不大,但翻车点非常集中。这一章整理了五个我在项目里见过最多、也亲自踩过的问题,每条都按“现象 → 原因 → 解决”的路径写清楚。调试的时候对着查,能省不少时间。

5.1 过渡动画没生效:瞬变而非伸缩

现象:点击按钮后侧边栏瞬间从 220px 变成 64px,或者反过来瞬间变宽,transition像不存在一样。

原因有三个,按出现频率排:第一,transition写在了.collapsed里,只对折叠态生效,展开时没有过渡;第二,元素的初始width是auto,过渡无法从不确定的值开始;第三,transition-property写错属性名,比如实际变化的是width,你写的是margin-left。

解决:transition写在默认状态,不要写在折叠态。初始宽度必须是具体数值。用 DevTools 的 Computed 面板实时去看width的值,过渡进行时数字会逐帧变化;如果数字是一跳一跳的,说明宽度没有参与过渡,问题就出在属性名或初始值上。

.sidebar { width: 220px; transition: width 300ms cubic-bezier(0.4, 0, 0.2, 1); /* 写在这里 */ }

5.2 折叠后文字溢出,把侧边栏撑宽

现象:侧边栏折叠到 64px 后,菜单文字没有消失,而是从窄条右侧漏出来,或者把整个页面撑出横向滚动条。

原因:侧边栏宽度虽然变成了 64px,但菜单文字仍然按 220px 的内容宽度在渲染。overflow: hidden能挡住视觉溢出,但如果文字允许换行,内容自身就会把布局撑破。

解决:侧边栏加overflow: hidden,菜单文字加white-space: nowrap。如果想做得更细腻一些,给文字加一个淡出效果,视觉上比直接截断舒服:

.sidebar .link-text { transition: opacity 150ms ease; } .sidebar.collapsed .link-text { opacity: 0; }

淡出时长 150ms 比侧边栏的 300ms 短,效果是文字先消失、侧边栏再收窄,不会出现文字被挤压变形的中间态。如果你想让文字在收起过程中慢慢淡出而不是一开始就消失,就把透明度过渡的时长也改成 300ms,并且不加延迟,两者会同步进行。

5.3 fixed 布局下内容区被盖住或跳动

现象:侧边栏设置position: fixed后,内容区有一部分被侧边栏盖住;或者展开时内容区突然被挤开一下,又恢复正常。

原因:内容区没有设置margin-left,或者设置了但没加transition。fixed 元素脱离文档流,内容区不会自动让位。展开时内容区的跳动则是因为折叠态和展开态的margin-left切换没有过渡,宽度一变,内容区直接跳到位。

解决:给内容区设置margin-left: 220px,并在折叠态通过兄弟选择器改成64px,两个宽度之间加上和侧边栏一致的过渡参数。注意margin-left要写在内容区,不是写在侧边栏上。侧边栏自己已经 fixed 了,写margin-left对它没有意义。

5.4 子菜单展开时高度塌陷与双重过渡打架

现象:侧边栏处于展开状态时,点击父菜单展开子菜单,一切正常;但侧边栏折叠后,再操作父菜单,子菜单出现高度突然矮一截、或者整个页面连续跳动的现象。

原因:侧边栏的width过渡和子菜单的max-height过渡同时触发,两个过渡叠加在同一个视觉区域上,渲染帧率会掉,观感上就是“打架”。另外,折叠态下子菜单的内容高度被max-height: 0收掉了,但子菜单里的子项还保持着原来的高度计算,展开时就会先跳到max-height的上限附近再回落。

解决:把两个动画拆到不同状态触发。侧边栏折叠时禁用子菜单动画,直接瞬间隐藏;侧边栏展开时再恢复子菜单过渡。用transition: none强制取消折叠态下的子菜单过渡:

.sidebar.collapsed .submenu { max-height: 0 !important; transition: none; }

如果你不想用!important,可以把子菜单展开的选择器改成限定只在非折叠态生效:

.sidebar:not(.collapsed) .submenu.open { max-height: 300px; }

这样写,折叠侧边栏时子菜单不会进入过渡状态,展开侧边栏后子菜单的动画行为也完全可控。这个细节不处理的话,侧边栏的观感会始终有一种“没调好”的微妙感,很多人会归咎于动画帧率,其实是两个过渡在互相干扰。

5.5 点击穿透:遮罩层挡了菜单点击

现象:移动端展开侧边栏后,点击遮罩层空白处,侧边栏收起的同时,点击事件穿透到了下面的菜单项或内容区按钮;或者点击遮罩时遮罩先消失,第二次点击才真正触发下层元素。

原因:遮罩层隐藏时仍然占据点击区域,或者display: none切换导致过渡期间遮罩层缺失、点击直接落到下层元素上。另一个常见原因是没有做事件隔离,菜单的 click 事件冒泡到了遮罩层。

解决:遮罩层隐藏状态加visibility: hidden和pointer-events: none,显示状态恢复pointer-events: auto。

.sidebar-mask { opacity: 0; visibility: hidden; pointer-events: none; transition: opacity 300ms ease, visibility 300ms ease; } .sidebar-mask.active { opacity: 1; visibility: visible; pointer-events: auto; }

visibility: hidden会让元素不响应点击,而pointer-events: none是双保险。菜单项的 click 事件里,如果有需要阻止冒泡的场景,用e.stopPropagation()收一下。这个组合处理完,点击穿透才算真正堵住。

6. 进阶技巧:响应式断点与记住折叠状态

基础功能稳定之后,再往前进两步,这两步也是实际项目中经常加的需求:窄屏下切换成抽屉式侧边栏,以及记住用户上次是展开还是折叠。

6.1 用 matchMedia 切换两种形态

桌面端用折叠式,移动端用抽屉式,这是一个很常见的响应式需求。折叠式里侧边栏始终占据布局空间,抽屉式里侧边栏默认滑出屏幕,只有打开时才盖在内容区上面。

const tabletQuery = window.matchMedia('(max-width: 768px)'); function applyMode(isMobile) { if (isMobile) { sidebar.classList.add('collapsed'); mask.classList.remove('active'); } else { sidebar.classList.remove('collapsed'); mask.classList.remove('active'); } } tabletQuery.addEventListener('change', (e) => applyMode(e.matches)); applyMode(tabletQuery.matches);

matchMedia的768px断点要和 CSS 里的@media断点保持一致。切换断点时把遮罩层状态重置掉,避免从宽屏切到窄屏后遮罩层残留。这里用的是addEventListener,如果你要兼容老版本 Safari,需要换成addListener,效果相同。

6.2 用 localStorage 记住折叠状态

记住用户偏好是个加分项,很多后台系统都做了,做法不复杂:

const STORAGE_KEY = 'sidebar-collapsed-v1'; function restoreState() { if (localStorage.getItem(STORAGE_KEY) === '1') { sidebar.classList.add('collapsed'); } else { sidebar.classList.remove('collapsed'); } } collapseBtn.addEventListener('click', () => { const collapsed = sidebar.classList.toggle('collapsed'); localStorage.setItem(STORAGE_KEY, collapsed ? '1' : '0'); }); restoreState();

localStorage只能存字符串,所以布尔值要转成'1'和'0'。key 里加一个v1版本号是我的习惯,以后如果折叠逻辑改了,直接换v2,旧数据不会干扰新逻辑。恢复代码放在script.js底部,会有一次闪烁:侧边栏先展开再收起。想避免这个闪烁,就把restoreState()内联进head里的脚本,虽然会让首屏多一点点解析时间,但用户感知更顺。

验证这个功能很简单:收起侧边栏、刷新页面,侧边栏应该保持收起;清空 localStorage 再刷新,恢复默认的展开状态。我在项目里最后一步通常会打开 DevTools 的 Performance 面板,把折叠动画跑一遍,看渲染帧率有没有掉到 30fps 以下。这种小交互,没人会拿性能分析仪去查,但动画流畅与否,用户身体是能感受到的。希望这些经验帮你在做侧边伸缩导航栏时少走几步弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询