← 返回文章

静态博客前端:分层外观与可访问性

更新于

把画布、遮罩、实色纸面和玻璃浮层拆开:配色与画布独立,长文不依赖透明度,回退路径始终可读。

深绿画布、浅色遮罩、实色纸面与玻璃顶栏四层依次叠放

先想一个很常见的组合:背景是一张照片,正文是半透明卡片,顶栏再来一层毛玻璃。截图里看着有层次,真正读长文时会发现,背景越花,文字越难读;为了救可读性,又把透明度一降再降,最后玻璃也没了,层次也没了。

更稳的做法是把“氛围”和“阅读”分开:画布负责背景,遮罩统一明暗,正文和卡片用实色纸面,玻璃只留给顶栏和播放器这类真正浮在内容上的控件。

这篇用普通 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 测试可以验证属性和禁用状态,但不能证明滤镜绘制、性能或整站可访问性已经通过。最终仍要在真实画布、窄屏和缩放阅读下核对。

技术参考