核心概念

主题系统

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

命名规律

  1. 不带后缀 = 背景色
  2. -foreground = 该背景上的文字色
  3. -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

ktr/template/style.css
@import 'tailwindcss';
@import '@karinjs/template-react/styles';
@source './**/*.{ts,tsx}';

三行分工:

行号作用
1Tailwind CSS v4 核心
2ktr 样式基座(含 HeroUI 全套样式与默认主题变量)
3Tailwind 扫描模板目录(按需生成 CSS)

不要再写 @theme { --color-accent: var(--accent); ... } 这类颜色映射块。 HeroUI 已用 @theme inline 做好桥接,普通 @theme 会把它盖掉,导致元素级主题注入失效。

自定义主题

方法 1:CSS 变量覆盖

style.css 的三行之后添加自定义变量:

ktr/template/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:可视化主题编辑器

开发面板右侧的模板主题抽屉提供可视化调整:

  1. 选择预设主题(11 种)
  2. 调整明暗模式
  3. 自定义强调色、基础色
  4. 调整圆角、字体

调整后点击**「查看代码」**,生成对应 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 二值的 classdata-theme[data-theme='red'] 这类自定义主题选择器在产物上永远不会命中。多主题换肤的正确做法是在调用侧按主题下发变量值——通过 theme 的语义色字段或 theme.vars 直通口:

ktr/template/style.css
@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-darkforeground-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">阴影效果</>}
      </>
    )
  }
})

明暗切换原理

框架通过两种机制触发深色模式:

  1. .dark 类名 —— ktr 在样式基座里用 @custom-variantdark: 重定义为类选择器驱动(Tailwind v4 默认编译成 prefers-color-scheme 媒体查询,在截图浏览器的浅色偏好下永不命中)
  2. [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--accent
  • accentForeground--accent-foreground
  • background--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 但组件颜色未变化

排查步骤

  1. 检查是否使用语义 token
// ❌ 硬编码颜色不受主题影响
export const  = < ="bg-white text-black" />

// ✅ 语义 token 自动跟随主题
export const  = < ="bg-background text-foreground" />
  1. 检查变量是否正确注入
# 查看生成的 HTML
cat dist/template/html/hello_card.html | grep 'style='

# 应看到:
# <body class="dark" data-theme="dark" style="--accent: oklch(...); ...">
  1. 确认 CSS 选择器优先级
/* ❌ 普通 @theme 会覆盖 inline */
@theme {
  --color-accent: blue; /* 这会盖掉 style="--accent: ..." */
}

/* ✅ 不要手写 @theme,HeroUI 已处理 */

问题 2:深色模式不切换

现象:设置 theme.mode = 'dark' 但样式未变化

排查步骤

  1. 检查 CSS 选择器
/* ✅ 同时支持两种选择器 */
.dark,
[data-theme='dark'] {
  --background: oklch(0.12 0 0);
}

/* ❌ 只用其中一种可能不生效 */
.dark {
  --background: oklch(0.12 0 0);
}
  1. 检查 HTML 类名
<!-- ✅ 正确 -->
<body class="dark" data-theme="dark">
  <!-- ❌ 缺少类名 -->
  <body data-theme="dark"></body>
</body>

问题 3:颜色不一致

现象:同一模板在面板和截图中颜色不同

原因:面板调整主题后未传给生产渲染

解决

  1. 面板「查看代码」生成 CSS,固化到 style.css
  2. 或在生产渲染时显式传 theme
await ('hello/card', , {
  : {
    : 'dark',
    : 'oklch(0.62 0.19 254)' // 与面板设置保持一致
  }
})

下一步

On this page