应用外壳 · app-shell

工具型页面的骨架:左边一列常驻导航、上面一条工具栏、右边是主区。 它存在的理由不是"少写几个 div",而是两个每个人都会踩的坑 —— 见下面两节。

1 · 标准三段式

应付账款 · 2025 年度

已同步 5 分钟前
主区内容
主区内容
主区内容

当前页靠三条线索标记:左侧 3px 竖条、底色、字重加一档 —— 不只靠底色,灰度打印也分得出来。而且它同时带了 aria-current="page",读屏用户听到的是"总览,当前页"。

2 · 坑之一:宽内容会撑破主区(两个保险)

❌ 常见写法:宽表格直接放主区

一行 30rem 宽的表格式内容

两件事都没做:min-width 保持默认的 auto ("不小于内容的最小宽度"),宽表格也没有自己的滚动容器。 于是 grid 轨道被内容顶开,主区被撑到栏外 —— 侧栏、顶栏一起被推出可视区。

✅ 本库:min-width: 0 + .shell__scroll

一行 30rem 宽的表格式内容

主区宽度锁在栏宽内(min-width: 0),宽内容放进 .shell__scroll 里自己横滚 —— 侧栏和顶栏不动,页面不横滚。

这就是 .shell__main { min-width: 0 } 存在的全部理由。 它是整个组件里最容易漏、后果最大的一行:漏了以后页面在 1280px 屏幕上看着正常, 一旦数据表多两列就横滚 —— 而且你会先怀疑表格,不会怀疑外壳。

3 · 坑之二:高度用 100dvh,不是 100vh

这一条在桌面浏览器上看不出来,只能在手机上看到, 所以没有截图可摆 —— 但它是本组件写 min-height: 100vh; min-height: 100dvh 两行的原因。

4 · 侧栏更窄:只有图标

图标态的名字必须来自可见文字 .shell__link-label —— 它被视觉隐藏(1px 盒 + 裁切)但仍在可访问树里,读屏念的是「总览」而不是「链接」。 改用 aria-label 也能让读屏念对,但那是第二份真值:改了可见文字忘了改它, 念的还是旧的;更关键的是抽屉里那份克隆补不回文字(克隆离开 .shell--rail 作用域,隐藏规则失效,但 aria-label 不会变成可见文字)。 title 只负责鼠标悬停提示,不负责名字。

5 · 主区自己滚(看板型)

只有主区滚动

内容 1
内容 2
内容 3
内容 4
内容 5
内容 6
内容 7
内容 8

.shell--scroll-main 让侧栏与顶栏完全不动,只有主区滚 —— 适合看板、监控墙。⚠️ 用这个变体时不要再让页面本身滚:两个滚动容器会让 "滚到底"的行为不可预期(用户滚的是哪一个?)。

6 · 窄屏:侧栏收走,顶栏出一个菜单按钮

把浏览器窗口缩到 768px 以下。侧栏整列消失, 主区铺满,顶栏起始边出现一个 ☰ 按钮 —— 点它打开抽屉。 Esc 关、点遮罩关、关闭后焦点回到按钮上,这三件事来自 03-patterns/drawer,本组件只做接线(shell-nav.js)。

应付账款 · 2025 年度

已同步 5 分钟前
缩到 768px 以下,左上角会多出一个 ☰
抽屉里的列表是侧栏的克隆,当前页标记也一起带过去

上面第 1–5 节没有引 shell-nav.js,所以它们缩窄后没有按钮, 侧栏直接消失 —— 缩窄窗口看看就知道了:一个入口都没有。 所以这层脚本在 START-HERE 里归「必需」,不是"可选增强": "宽屏没有它一切照旧"只对桌面成立,而用户在手机上就走不出去了。

⚠️ 别在这里自己写一个滑出来的侧栏。抽屉要的不是"滑出来", 而是焦点陷阱 / 焦点归位 / 背景 inert / 锁滚动 / Esc 五件事。 自己写必然漏两件,而漏掉的那两件在鼠标用户眼里完全正常。

7 · 窄侧栏 + 抽屉:图标在桌面,文字在抽屉

把窗口缩到 768px 以下再点 ☰: 抽屉里是图标 + 文字。同一份标记 —— 不是第二份导航,也没有 JS 分支。

图标导航

桌面:侧栏只有图标(宽 4.5rem)
窄屏:点 ☰,抽屉里是图标 + 文字

文字包在 .shell__link-label 里:rail 状态下它被视觉隐藏, 但仍在可访问树里(读屏念「总览」,不是「链接」)。抽屉里的克隆被挂到 <body> 下,离开了 .shell--rail 的作用域, 隐藏规则自然不再命中 —— 文字自己就回来了。所以不能把文字删掉、 也不能用 display: none:前者抽屉里只剩一排图标,后者连名字都没了。

只做纯图标栏、不接抽屉的话,第 4 节那种写法(图标 + aria-label) 也成立;但只要你会用 data-shell-nav,就把文字留在 DOM 里 —— 一份标记同时满足两种形态。

8 · 平面外壳(shell--flat):融进纸面,不画第二层框

内嵌在报告里的外壳

外壳底色改回纸色(--paper)
主区边线透明 ⇒ 不与外层的卡片边框重影

什么时候用:外壳本身已经在卡片 / 弹窗 / 报告版式里。 这时它再自带一层底色和左边线,就会和外层边框形成"双重描边" —— 两条相距 1px 的线,看上去像渲染错位。
什么时候别用:外壳是整页容器。那时它需要自己的边界, --flat 会让侧栏和主区糊成一片。