静态博客前端:分层外观与可访问性
· 前端技术 · 约 6 分钟阅读
更新于
把画布、遮罩、实色纸面和玻璃浮层拆开:配色与画布独立,长文不依赖透明度,回退路径始终可读。
先想一个很常见的组合:背景是一张照片,正文是半透明卡片,顶栏再来一层毛玻璃。截图里看着有层次,真正读长文时会发现,背景越花,文字越难读;为了救可读性,又把透明度一降再降,最后玻璃也没了,层次也没了。
更稳的做法是把“氛围”和“阅读”分开:画布负责背景,遮罩统一明暗,正文和卡片用实色纸面,玻璃只留给顶栏和播放器这类真正浮在内容上的控件。
这篇用普通 HTML、CSS 和 JavaScript 说明这套分层。配色、画布和浮层透明度是三个互不重置的选择;纸面始终可用;浏览器不支持玻璃时也有明确的实色回退。
背景、遮罩、纸面与玻璃的绘制顺序
三个选择,各管各的
| 选择 |
取值 |
作用范围 |
| 配色 scheme |
light / dark / system |
文字、纸面、边框和强调色 |
| 画布 canvas |
mist / night / paper |
页面背后的本地图片 |
| 浮层透明度 opacity |
0–90 的整数 |
顶栏和播放器底色的 alpha |
透明度 90% 表示底色 alpha 是 0.1,不是 0.9:alpha = 1 - transparency / 100。它只改变浮层背景色,不要对包含正文的元素设置 opacity,否则文字、图片和按钮会一起变淡。正文纸面永远是实色。
背景、遮罩、面板分开画
背景图固定在页面背后,遮罩单独一层,内容纸面再叠在遮罩上。遮罩不承载文字,也不使用会改变正文颜色的整页滤镜。
:root { --page: #e6e9e1; --surface: #fbfaf5; --ink: #1c241d; --veil: rgba(232, 235, 227, .28); --panel-rgb: 251, 250, 245; --panel-alpha: .70; --panel-background: rgba(var(--panel-rgb), var(--panel-alpha)); --reading-backdrop: blur(18px) saturate(1.1); }
html { background: var(--page); } body { margin: 0; background: transparent; color: var(--ink); } body::before { content: ""; position: fixed; inset: 0; z-index: -3; background: var(--background-image) center / cover no-repeat; } body::after { content: ""; position: fixed; inset: 0; z-index: -2; pointer-events: none; background: radial-gradient(ellipse 120% 65% at 50% -8%, rgba(255, 253, 246, .30), transparent 62%), var(--veil); } .article-main, .post-card, .site-footer { background: var(--surface); } .site-header { background: var(--panel-background); -webkit-backdrop-filter: var(--reading-backdrop); backdrop-filter: var(--reading-backdrop); }
|
body 自己必须透明,否则不透明的 body 背景会盖住 z-index: -3 的画布。负层级只负责绘制顺序,不能把正文也放进负层级。
一张纸就够了
玻璃面只保留两处:常驻顶栏和播放器悬浮控件。文章纸面、卡片、弹层、搜索框都用实色表面。不要在一个玻璃容器里再套一层玻璃卡片;半透明面板叠加后,最终颜色不再等于单层 alpha,嵌套背景滤镜还会增加合成开销。
弹层虽然浮在内容上,但可以是不透明纸面。这样设置面板、搜索对话框和播放器展开层都不依赖背景滤镜,也不会出现“每层都在模糊”的噪声。
读取偏好,先校验再应用
所有值都经过枚举或范围校验;非法存储值回到默认。外观脚本在 <head> 里同步执行,先写根元素属性再绘制,避免刷新时闪一下。
const SCHEMES = ['light', 'dark', 'system']; const CANVASES = ['mist', 'night', 'paper']; const root = document.documentElement; const read = (key) => { try { return localStorage.getItem(key); } catch { return null; } }; const save = (key, value) => { try { localStorage.setItem(key, value); } catch { } }; const oneOf = (value, allowed, fallback) => allowed.includes(value) ? value : fallback;
let scheme = oneOf(read('scheme'), SCHEMES, 'system'); let canvas = oneOf(read('canvas'), CANVASES, 'mist'); const savedOpacity = read('opacity'); const parsedOpacity = savedOpacity === null ? NaN : Number(savedOpacity); let opacity = Number.isFinite(parsedOpacity) ? Math.min(90, Math.max(0, parsedOpacity)) : 30;
const apply = () => { root.dataset.scheme = scheme === 'system' ? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : scheme; root.dataset.schemePreference = scheme; root.dataset.canvas = canvas; root.style.setProperty('--panel-alpha', (1 - opacity / 100).toFixed(2)); }; apply();
document.querySelectorAll('[data-scheme]').forEach((button) => { button.addEventListener('click', () => { scheme = button.dataset.scheme; save('scheme', scheme); apply(); }); });
|
如果允许“跟随系统”,还要监听 matchMedia 的变化,并在系统配色改变时重新计算有效配色。但不要覆盖用户保存的 system 选择。
回退不是一句“支持降级”
实色纸面是默认路径,不是“玻璃失败”的别名。其余回退各有原因:
- 系统减少透明度:用户显式选择跟随系统时,把浮层改为实色。
- 不支持背景滤镜:
@supports 检测失败时,顶栏和播放器直接使用实色,不保留半透明底色。
- 强制颜色:使用
Canvas、CanvasText、LinkText 等系统语义色,并关闭背景图和遮罩。
- 减少动态效果:停止唱片旋转和面板位移,不影响内容。
@media (prefers-reduced-transparency: reduce) { :root[data-panel-preference="system"] { --panel-background: var(--surface); --reading-backdrop: none; } } @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { :root { --panel-background: var(--surface); --reading-backdrop: none; } } @media (forced-colors: active) { :root { --page: Canvas; --surface: Canvas; --surface-muted: Canvas; --ink: CanvasText; --ink-strong: CanvasText; --muted: CanvasText; --faint: CanvasText; --accent: LinkText; --accent-deep: LinkText; --border: CanvasText; --panel-background: Canvas; } body::before, body::after { display: none; } }
|
对比度要算合成后的颜色
实色纸面上的对比度只比较文字和 --surface;玻璃浮层则要先把半透明底色与背后的画布合成,再计算相对亮度。简化估算可以用:
C = alpha × panel + (1 − alpha) × background
|
再按 sRGB 相对亮度计算对比度。对于 blur 这类空间滤镜,这个逐色近似覆盖不了真实照片的全部情况,所以正文不要依赖玻璃,浮层文字也要在实际画布上复核。
验收状态,也验收性能
先检查状态,再检查绘制:
- 默认浅色纸面;切换深色不改变画布和透明度。
- 透明度滑块只影响顶栏和播放器,正文纸面不变。
- 刷新后合法值恢复;非法枚举和越界数值回到默认。
- 拒绝存储时,当前页仍能切换。
- 系统减少透明度、无滤镜和强制颜色路径下,说明文字和实际效果一致。
- 窄屏面板不超出视口,键盘焦点可见。
- 播放器展开时不全屏遮挡正文;收起后仍保留音频状态。
DOM 测试可以验证属性和禁用状态,但不能证明滤镜绘制、性能或整站可访问性已经通过。最终仍要在真实画布、窄屏和缩放阅读下核对。
技术参考