Known 组件库、后台应用、营销官网三处共用一套主题约定:CSS 自定义属性 + 单一 data-theme 属性 +
prefers-color-scheme 回落 + localStorage 持久化。不共享代码、只共享变量命名。
变量分层
:root {
/* 品牌 */
--brand: 靛蓝; --brand2: 亮靛; --brand-ink: 靛深;
--teal; --violet; --amber; --rose; --green;
/* 代码面板(两主题共用深色) */
--code-bg; --code-line; --code-fg;
/* 字体与圆角 */
--disp; --sans; --mono; --r; --maxw;
}
:root, [data-theme="light"] {
/* 瓷白 porcelain */
--bg; --bg2; --surface; --surface2;
--ink; --ink2; --muted; --faint;
--line; --line2; --brand-soft; --chrome;
--ok-fg; --warn-fg; --no-fg;
--ok-bg; --warn-bg; --no-bg; --info-bg;
}
[data-theme="dark"] {
/* 墨蓝 ink-navy;同名变量覆盖 */
}
语义层(--brand --ok-fg --line)与具体值分离,切主题只换值不换类。
首屏无闪烁
App.razor 的 <head> 里有一段 pre-paint 脚本(营销站示例):
<script>
(function () {
try {
var t = localStorage.getItem('known-theme');
if (!t) t = matchMedia('(prefers-color-scheme:dark)').matches ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', t);
} catch (e) {
document.documentElement.setAttribute('data-theme', 'light');
}
})();
</script>
这段必须早于任何 <link rel=stylesheet>——浏览器拿到 head 就同步执行,
CSS 变量按最终主题的 <link> 应用,页面渲染时主题已定。这就是无闪烁的关键。
try/catch 兜的是 localStorage 在隐私模式或某些 WebView 下抛异常的场景;
兜底走 light 保证页面始终有主题、不裸奔。
组件里怎么用
组件 razor 不判断"当前是深色还是浅色",一律写语义变量:
.known-button.primary {
background: var(--brand);
color: #fff;
}
.known-card {
background: var(--surface);
border: 1px solid var(--line);
}
主题切换是浏览器层的事:CSS 变量值变了、组件自动跟随、组件逻辑无感。
LocalService
Known 框架侧的 LocalService 提供:
Info.IsDark— 只读,当前主题。ToggleThemeAsync()— 切换并写入localStorage('known-theme'),触发Changed事件。Changed— 供需要"主题变了要重画 canvas"的场景订阅。
后台应用与 Known 组件里做主题按钮时优先用 LocalService,营销站因为是静态 SSR 只能靠
site.js 里的原生 JS 孤岛自己 toggle data-theme——两种形态共享同一份 known-theme localStorage key,
用户在任一子域切了主题、另一子域打开也保持。
有意不做
- 主题市场 / 用户自定义色板:Batch 5 之后;当前只有
light/dark两态。 - 跟随系统自动切换(不持久化):可选实现;当前只有
localStorage未写入时才回落系统偏好。
与其他概念
- 代码面板在两主题下都用
--code-bg(深色),是因为代码本身在深色下阅读体验最好、 这个决定属定案、不随主题翻转。 - 组件库的状态色(
--ok-*--warn-*--no-*)在深浅色下值不同、语义相同; 见/docs/guides/layout-composition的实例。
扫码关注公众号
扫码加入 QQ 群