主题系统
HeroUI 主题变量体系、主题注入时机和自定义主题实现
ktr 的主题系统基于 HeroUI v3,提供语义化的颜色 token 和灵活的主题定制能力。
HeroUI 主题变量体系
核心设计原则
HeroUI 使用语义化命名而非硬编码颜色:
// ❌ 避免硬编码颜色
export const = < ="bg-white text-gray-900 border-gray-200" />
// ✅ 使用语义 token
export const = < ="bg-background text-foreground border-border" />好处:
- ✅ 自动适配明暗模式
- ✅ 统一品牌色调整
- ✅ 主题切换无需修改组件代码
颜色 token 清单
| Token | 作用 | Tailwind 类名 |
|---|---|---|
| 基础色 | ||
background | 页面根背景 | bg-background |
foreground | 主要文字 | text-foreground |
surface | 卡片、面板表面 | bg-surface |
surface-foreground | 表面上的文字 | text-surface-foreground |
muted | 次要文字 | text-muted |
border | 边框、分割线 | border-border |
| 强调色 | ||
accent | 主强调色背景 | bg-accent |
accent-foreground | 强调色上的文字 | text-accent-foreground |
accent-soft | 浅色强调背景 | bg-accent-soft |
accent-soft-foreground | 浅色背景上的文字 | text-accent-soft-foreground |
| 状态色 | ||
success / success-foreground | 成功状态 | bg-success / text-success-foreground |
warning / warning-foreground | 警告状态 | bg-warning / text-warning-foreground |
danger / danger-foreground | 危险/错误状态 | bg-danger / text-danger-foreground |
success-soft / warning-soft / danger-soft | 状态浅色背景 | bg-success-soft 等 |
| 中性色 | ||
default / default-foreground | 中性填充 | bg-default / text-default-foreground |
overlay / overlay-foreground | 浮层背景 | bg-overlay / text-overlay-foreground |
命名规律
- 不带后缀 = 背景色
-foreground= 该背景上的文字色-soft= 浅色背景变体(低饱和度,适合状态标签)
// 示例:强调色卡片
export const = < ="bg-accent text-accent-foreground p-4 rounded-lg">主强调色卡片</>
// 示例:浅色状态标签
export const = < ="bg-success-soft text-success-soft-foreground px-2 py-1 rounded">已完成</>样式入口
所有模板共用 ktr/template/style.css:
@import 'tailwindcss';
@import '@karinjs/template-react/styles';
@source './**/*.{ts,tsx}';三行分工:
| 行号 | 作用 |
|---|---|
| 1 | Tailwind CSS v4 核心 |
| 2 | ktr 样式基座(含 HeroUI 全套样式与默认主题变量) |
| 3 | Tailwind 扫描模板目录(按需生成 CSS) |
不要再写 @theme { --color-accent: var(--accent); ... } 这类颜色映射块。 HeroUI 已用 @theme inline
做好桥接,普通 @theme 会把它盖掉,导致元素级主题注入失效。
自定义主题
方法 1:CSS 变量覆盖
在 style.css 的三行之后添加自定义变量:
@import 'tailwindcss';
@import '@karinjs/template-react/styles';
@source './**/*.{ts,tsx}';
/* 自定义主题变量 */
:root {
--accent: oklch(0.62 0.19 254); /* 蓝紫色 */
--accent-foreground: oklch(1 0 0); /* 白色文字 */
--background: oklch(0.98 0 0); /* 浅灰背景 */
--foreground: oklch(0.15 0 0); /* 深灰文字 */
--border: oklch(0.88 0 0); /* 边框色 */
--radius: 0.5rem; /* 圆角 */
}
/* 深色模式 */
.dark,
[data-theme='dark'] {
--accent: oklch(0.72 0.16 254);
--accent-foreground: oklch(0.15 0 0);
--background: oklch(0.12 0.01 254);
--foreground: oklch(0.95 0 0);
--border: oklch(0.25 0 0);
}HeroUI 变量使用 oklch() 颜色空间,优势:
- ✅ 感知均匀(亮度线性变化)
- ✅ 支持 P3 色域(更鲜艳)
- ✅ 易于调整(独立控制亮度、饱和度、色相)
格式:oklch(L C H) 或 oklch(L C H / A)
- L:亮度(0-1,0=黑色,1=白色)
- C:色度/饱和度(0-0.4,0=灰色)
- H:色相(0-360,度数)
- A:透明度(可选,0-1)
方法 2:可视化主题编辑器
开发面板右侧的模板主题抽屉提供可视化调整:
- 选择预设主题(11 种)
- 调整明暗模式
- 自定义强调色、基础色
- 调整圆角、字体
调整后点击**「查看代码」**,生成对应 CSS:
/* 生成的主题代码 */
:root {
--accent: oklch(0.62 0.19 254);
--accent-foreground: oklch(1 0 0);
--background: oklch(0.98 0 0);
--foreground: oklch(0.15 0 0);
--surface: oklch(1 0 0);
--surface-foreground: oklch(0.15 0 0);
--border: oklch(0.88 0 0);
--radius: 0.5rem;
--font-sans: Inter, sans-serif;
}复制粘贴到 style.css 即可固化。
方法 3:多主题切换
框架产物(SSR 外壳和面板沙盒)只写 light / dark 二值的 class 和 data-theme,[data-theme='red'] 这类自定义主题选择器在产物上永远不会命中。多主题换肤的正确做法是在调用侧按主题下发变量值——通过 theme 的语义色字段或 theme.vars 直通口:
@import 'tailwindcss';
@import '@karinjs/template-react/styles';
@source './**/*.{ts,tsx}';
/* 默认主题(蓝色),不传主题时的兜底 */
:root {
--accent: oklch(0.62 0.19 254);
}
/* 深色模式仍按明暗二值组织 */
.dark,
[data-theme='dark'] {
--background: oklch(0.12 0 0);
--foreground: oklch(0.95 0 0);
}运行时切换:
// 默认主题(不传 theme,组件库自身主题生效)
await ('hello/card', )
// 红色主色
await ('hello/card', , {
: { : 'light', : 'oklch(0.72 0.22 27)' }
})主题注入时机
框架不发明默认主题色。 ctx.theme 只包含调用方显式提供的字段;没人显式设置时,SSR
和面板沙盒都不注入任何颜色变量,组件库按自身默认主题渲染。
开发态:面板调试
「模板主题」抽屉里显式调整后,面板把设置经 ctx.theme 传给沙盒:
function (: any) {
// 下发给沙盒
.?.(
{
: 'ktr-panel',
: 'ktr:theme',
: { }
},
..
)
}沙盒接收后注入变量并重新渲染:
.('message', () => {
if (..type === 'ktr:theme') {
const { } = ..payload
// 注入 CSS 变量并切换明暗类名(只写显式提供的字段)
()
// 重新渲染当前模板
()
}
})生产态:SSR 渲染
渲染选项显式传 theme 字段:
await ('hello/card', , {
: {
: 'dark',
: 'oklch(0.62 0.19 254)',
: 'oklch(0.12 0.01 254)',
: 'oklch(0.95 0 0)'
}
})框架把变量写到 HTML 的 <body> 上:
<body
class="dark"
data-theme="dark"
style="--accent: oklch(0.62 0.19 254); --background: oklch(0.12 0.01 254); --foreground: oklch(0.95 0 0)"
>
<div id="container">
<!-- 组件内容继承变量 -->
</div>
</body>实现代码:
export function (: string, : { ?: <ThemeContext> }): string {
const { } =
// 生成主题变量(只输出显式提供的字段)
const = ? () : ''
const = ?.
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>${}</style>
</head>
<body class="${ === 'dark' ? 'dark' : ''}"${ ? ` data-theme="${}"` : ''} style="${}">
<div id="container">${}</div>
</body>
</html>`
}
function (: <ThemeContext>): string {
const : string[] = []
// 只输出显式提供的字段
if (. !== ) .(`--accent: ${.}`)
if (. !== ) .(`--accent-foreground: ${.}`)
if (. !== ) .(`--background: ${.}`)
if (. !== ) .(`--foreground: ${.}`)
if (. !== ) .(`--surface: ${.}`)
if (. !== ) .(`--border: ${.}`)
// vars 直通口:圆角、字体等任意变量从这里下发
for (const [, ] of .(. ?? {})) {
.(`${}: ${}`)
}
return .('; ')
}组件中访问主题
模板组件通过 ctx.theme 访问主题设置:
// @filename: ./index.tsx
import { } from '@karinjs/template-react'
export interface HelloCardData {
: string
: <{ : string; : string }>
}
export default <HelloCardData>({
: 'Hello 卡片',
: ({ , }) => {
const = ?.?. === 'dark'
return (
< ="p-6 bg-surface rounded-lg">
< ="text-xl font-bold text-foreground">
{.}
</>
{/* 深色模式下隐藏投影 */}
< ={`mt-4 ${ ? '' : 'shadow-sm'}`}>
{..( => (
< ={.} ="flex justify-between">
< ="text-muted">{.}</>
< ="font-medium">{.}</>
</>
))}
</>
</>
)
}
})ctx.theme 的每个字段都可能不存在,消费时始终用可选链(?.)或默认值。深色模式
CSS 适配方式
HeroUI v3 没有 surface-dark、foreground-dark 这类 -dark 后缀 token——语义色的明暗值由 CSS 变量承载,框架切换 .dark 类时变量自动翻转,一个类名即可:
// ✅ 语义 token 单类写法,变量随 .dark 自动翻转
export const = (
< ="bg-surface border border-border">
< ="text-foreground">标题</>
< ="text-muted">描述</>
</>
)不需要用 dark: 变体逐个覆盖语义色;dark: 只留给明暗下真正不同的样式(如 shadow-sm dark:shadow-none)。框架自动处理明暗类名切换,组件无需关心。
逻辑判断方式
需要更复杂的明暗差异时,读取 ctx.theme?.mode:
import { } from '@karinjs/template-react'
export default ({
: '产品卡片',
: ({ , }) => {
const = .?. === 'dark'
// 深色模式下换图
const = ? '/logo-white.png' : '/logo-dark.png'
return (
< ="p-6 bg-surface">
< ={} ="Logo" />
{/* 深色下隐藏投影 */}
{! && < ="shadow-lg">阴影效果</>}
</>
)
}
})明暗切换原理
框架通过两种机制触发深色模式:
.dark类名 —— ktr 在样式基座里用@custom-variant把dark:重定义为类选择器驱动(Tailwind v4 默认编译成 prefers-color-scheme 媒体查询,在截图浏览器的浅色偏好下永不命中)[data-theme="dark"]属性 —— 兼容第三方组件库
/* HeroUI 样式基座同时支持两种选择器 */
.dark,
[data-theme='dark'] {
--background: oklch(0.12 0 0);
--foreground: oklch(0.95 0 0);
--surface: oklch(0.15 0 0);
--border: oklch(0.25 0 0);
}渲染时同时设置:
<body class="dark" data-theme="dark"></body>ThemeContext 类型定义
完整的主题变量结构(@karinjs/template-react 导出的 ThemeContext):
export interface ThemeContext {
/** 当前明暗模式。 */
: 'light' | 'dark'
// 强调色
: string
: string
: string
: string
// 基础色
: string
: string
: string
: string
: string
/** 任意 CSS 变量的直通口(圆角、字体、状态色等从这里下发),同名时以此为准。 */
: <string, string>
}
export interface RenderContext {
/** 当前渲染比例,截图模板通常保持为 1。 */
: number
/** 调用方显式提供的主题变量;缺省时组件库自身主题生效。 */
?: <ThemeContext>
}圆角(--radius)、字体(--font-sans)、状态色等更多变量不逐个加字段,统一经 vars 直通口下发。
字段名与 CSS 变量一一对应:
accent→--accentaccentForeground→--accent-foregroundbackground→--background
传 theme 和改 CSS 变量是同一套东西的两个入口。
最佳实践
1. 优先使用语义 token
// ✅ 推荐:语义化,自动适配主题
export const = < ="bg-surface text-foreground border-border" />
// ❌ 不推荐:硬编码颜色,无法换肤
export const = < ="bg-white text-gray-900 border-gray-200" />2. 主题变量只注入显式提供的字段
// ✅ 只传需要的字段
await ('hello/card', , {
: {
: 'dark',
: 'oklch(0.62 0.19 254)' // 只调整强调色
}
})
// ❌ 不要传完整的主题对象(除非真的需要)
await ('hello/card', , {
: {
: 'dark',
: 'oklch(0.62 0.19 254)',
: 'oklch(0.12 0 0)',
: 'oklch(0.95 0 0)',
: 'oklch(0.15 0 0)',
: 'oklch(0.25 0 0)'
// 还有更多字段……
}
})未提供的字段不会注入任何变量,组件库(HeroUI)自身主题生效。
3. 深色模式差异优先用 CSS
// ✅ 推荐:语义色随变量自动翻转,明暗样式差异用 dark: 变体
export const = < ="bg-surface shadow-sm dark:shadow-none" />
// ❌ 不推荐:简单样式差异不要用 JS 判断
const = .?. === 'dark' ? '' : 'shadow-sm'JS 判断适合复杂逻辑(换图、条件渲染)。
4. 自定义颜色用 oklch
/* ✅ 推荐:oklch 感知均匀,易调整 */
:root {
--accent: oklch(0.62 0.19 254); /* L=0.62 亮度,C=0.19 饱和度,H=254 蓝紫色相 */
}
.dark {
--accent: oklch(0.72 0.16 254); /* 深色下提高亮度,降低饱和度 */
}
/* ❌ 不推荐:hex/rgb 不直观,难调整明暗 */
:root {
--accent: #6366f1;
}oklch 调整技巧:
- 变暗:降低 L(亮度)
- 变浅:提高 L
- 降低饱和度:降低 C
- 换色相:调整 H(0=红,120=绿,240=蓝)
5. 多主题用 theme.vars 下发
框架外壳只写 light / dark 二值,[data-theme='red'] 这类自定义主题选择器在产物上永不命中。多主题在调用侧组织成变量集合,渲染时整体下发:
/* style.css 只准备默认主题与明暗两套变量 */
:root {
--accent: oklch(0.62 0.19 254); /* 默认蓝色 */
}
.dark,
[data-theme='dark'] {
--background: oklch(0.12 0 0);
}运行时通过 theme 的语义色字段(如 accent)或 theme.vars 直通口下发对应主题的变量值,即可完成换肤。
常见问题
问题 1:主题不生效
现象:设置 ctx.theme 但组件颜色未变化
排查步骤:
- 检查是否使用语义 token
// ❌ 硬编码颜色不受主题影响
export const = < ="bg-white text-black" />
// ✅ 语义 token 自动跟随主题
export const = < ="bg-background text-foreground" />- 检查变量是否正确注入
# 查看生成的 HTML
cat dist/template/html/hello_card.html | grep 'style='
# 应看到:
# <body class="dark" data-theme="dark" style="--accent: oklch(...); ...">- 确认 CSS 选择器优先级
/* ❌ 普通 @theme 会覆盖 inline */
@theme {
--color-accent: blue; /* 这会盖掉 style="--accent: ..." */
}
/* ✅ 不要手写 @theme,HeroUI 已处理 */问题 2:深色模式不切换
现象:设置 theme.mode = 'dark' 但样式未变化
排查步骤:
- 检查 CSS 选择器
/* ✅ 同时支持两种选择器 */
.dark,
[data-theme='dark'] {
--background: oklch(0.12 0 0);
}
/* ❌ 只用其中一种可能不生效 */
.dark {
--background: oklch(0.12 0 0);
}- 检查 HTML 类名
<!-- ✅ 正确 -->
<body class="dark" data-theme="dark">
<!-- ❌ 缺少类名 -->
<body data-theme="dark"></body>
</body>问题 3:颜色不一致
现象:同一模板在面板和截图中颜色不同
原因:面板调整主题后未传给生产渲染
解决:
- 面板「查看代码」生成 CSS,固化到
style.css - 或在生产渲染时显式传
theme
await ('hello/card', , {
: {
: 'dark',
: 'oklch(0.62 0.19 254)' // 与面板设置保持一致
}
})