1659 字
8 分钟

三月七主题插件设计

三月七主题插件设计#

在只有一个 Vue 项目时,主题通常只是组件库里的一份 CSS。

但当项目逐渐扩展到原生 Web Components、Astro 模板、博客和其它前端框架后,继续让每个项目各自维护颜色、圆角和暗色模式,会出现一个很直接的问题:页面看起来相似,实际使用的规则却越来越不一样。

@mar7th/march7th-theme 就是在这个阶段拆出来的。它不是某个组件库的附属样式,而是一套框架无关的视觉基础设施。

设计目的#

让视觉规则只有一个来源#

主题色、辅助色、背景层级、文字层级、边框、圆角、阴影和动效都属于跨项目共享的规则。它们不应该分别散落在 Vue 组件、原生组件和站点 CSS 中。

主题包把这些值整理成 Design Tokens。页面和组件只消费 Token,不再自己决定同一种按钮应该使用什么颜色、卡片应该有多大圆角。

不绑定具体框架#

主题能力需要同时服务:

  • Astro 输出的静态 HTML。
  • Vue 组件和 Vue Island。
  • 原生 Web Components。
  • React、Svelte 或传统 HTML 页面。

因此核心实现不能依赖 Vue 的响应式系统或 Astro 的组件生命周期。样式层使用 CSS Custom Properties,运行时只依赖标准 DOM API,框架只负责在自己的入口中完成初始化。

保留项目级定制空间#

共享视觉不等于所有项目只能使用同一种颜色。主题包提供稳定的默认值,同时允许项目调整主色相、辅助色相、状态色和局部 Token。

这样不同网站可以拥有自己的配色,但仍然共享相同的明暗关系、状态语义和组件结构。

方案#

Token 分层#

主题包没有只输出一组 --primary,而是把变量分成几个层级:

主题轴
├── 主色相与辅助色相
├── success / info / warning / danger 状态轴
└── light / dark / system 模式
基础色阶
├── primary 50–950
├── secondary 50–950
├── neutral 50–950
└── 四类状态色 50–950
语义 Token
├── 背景与前景
├── 操作与边框
├── 状态背景、边框和文字
├── 图表色
└── 圆角、阴影、时长和缓动

组件优先读取语义 Token,而不是直接绑定某一档颜色。以后调整底层色阶时,组件不需要跟着修改。

OKLCH 动态配色#

颜色使用 OKLCH 描述。与直接修改 RGB 或 HSL 相比,OKLCH 更适合在保持感知亮度的同时调整色相和色度。

主色只需要改变 --m7-hue,整套界面的基础色阶和语义色就会跟着变化。辅助色相可以独立调整,不需要和主色完全绑定。

import {
initializeTheme,
setThemeHue,
setThemeMode,
} from "@mar7th/march7th-theme"
initializeTheme({ defaultMode: "system", defaultHue: 215 })
setThemeMode("dark")
setThemeHue(285)

四类状态色与自动色阶#

成功、信息、警告和错误不能简单地由主色换一个 Hue 得到。它们承担明确的交互语义,也需要在浅色和深色界面中保持足够对比度。

主题包将 successinfowarningdanger 设计成四个独立角色,error 作为 danger 的兼容别名。每个角色只暴露色相、色度和明度三个输入轴,由生成器统一产生 50 到 950 的 11 档色阶。

import {
generateStatusPalette,
initializeStatusPalettes,
setStatusColor,
} from "@mar7th/march7th-theme"
initializeStatusPalettes({ gamut: "rgb" })
setStatusColor("success", {
hue: 145,
chroma: 0.17,
lightness: 0.62,
})
const preview = generateStatusPalette("warning", {
hue: 82,
chroma: 0.18,
lightness: 0.72,
gamut: "p3",
})
console.log(preview.scale[500], preview.contrast)

生成器通过 Culori 进行 RGB 或 Display P3 色域映射,并检查实色按钮文字和状态面板文字是否达到默认 4.5:1 的 WCAG AA 对比度。

这也是状态色编辑器中“通过 AA”的含义:当前配色在对应使用场景下达到了最低可读性要求,而不是单纯表示颜色生成成功。

主题控制器#

主题控制器统一处理:

  • 浅色、深色和跟随系统模式。
  • 主色相修改。
  • localStorage 持久化。
  • 系统主题变化监听。
  • 跨标签页同步。
  • 全局主题和局部主题作用域。
  • 强制色模式与减少动态效果设置。

控制器会维护根节点上的 class、data 属性和 CSS 变量,并发送 march7th-theme-readymarch7th-theme-change 等 DOM 事件。Vue、Astro 和原生脚本都可以订阅同一份状态。

首屏防闪烁#

SSG 和 SSR 页面如果等客户端脚本加载完成后再应用暗色主题,会先显示一次亮色页面。

主题包提供 createThemeInitScript(),生成一段只读取必要本地配置的首屏脚本。它在页面内容渲染前同步主题,不需要加载完整主题运行时。

import { createThemeInitScript } from "@mar7th/march7th-theme"
const themeInitScript = createThemeInitScript({
defaultMode: "system",
defaultHue: 215,
})

Web Components 控制面板#

主题包提供两个不依赖框架的控制组件:

  • <march7th-theme-control>:切换明暗模式和主色相。
  • <march7th-status-colors>:分别编辑四类状态色,查看完整色阶、AA 状态并导出 CSS 或 JSON。

它们使用 Shadow DOM,可以直接放进 Astro、Vue、React、Svelte 或普通 HTML 页面。组件发送可冒泡、可穿过 Shadow DOM 的 change 事件,因此框架外部也能获得变更结果。

兼容层#

新主题使用 --m7-* Token,但现有组件库和 shadcn 项目已经拥有自己的变量命名。一次性全部改写风险很高,因此主题包将兼容层作为正式出口:

  • compat/march7th.css 映射现有 March7th UI Vue 变量。
  • compat/shadcn.css 映射 --background--primary--border 等变量。
  • compat/generic.css 提供更通用的无前缀变量。

迁移可以逐步进行,而不是要求所有项目同时重写。

当前成果#

目前 @mar7th/march7th-theme 已发布 0.1.1

  • 完成框架无关的 CSS Token 和 TypeScript API。
  • 支持浅色、深色、系统模式和主题持久化。
  • 支持主色、辅助色以及四类状态色独立配置。
  • 支持状态色 50–950 色阶、RGB/P3 色域映射和 WCAG 对比度检查。
  • 提供首屏防闪烁脚本、主题控制组件和状态色编辑组件。
  • 提供 March7th、shadcn 和通用变量兼容层。
  • 已接入原生组件库、Vue 组件库和 Astro 项目模板。

边界#

主题包只负责视觉规则和主题状态,不负责按钮、弹窗等具体组件,也不决定页面布局。

这个边界很重要。博客、作品站、图集站和论坛可以拥有完全不同的页面结构,但只要读取同一套 Token,就能够保持一致的视觉语言。主题包统一的是规则,不是把所有网站做成同一个页面。

三月七主题插件设计
https://march7th.online/blog/posts/0045-三月七主题插件设计/
作者
Yiguo
发布于
2026-08-07
许可协议
CC BY-NC-SA 4.0
最后更新于 2026-08-07

目录