@civitai/theme

中文文档
0.5.2发布于 2026/10/04MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 1.7万

Framework-agnostic --civitai-* design tokens, derived build-time from civitai's real Mantine theme. CSS + typed JS + W3C DTCG.

原始使用文档
文档目录6 节
  1. Why generated (not hand-authored)
  2. Scheme-paired tokens
  3. Breakpoints — 🔴 the px scale, not Mantine's em scale
  4. Usage
  5. Recolouring a token (brand overrides)
  6. Build

@civitai/theme

Framework-agnostic design tokens, derived at build time from civitai's real Mantine theme. Ships three forms of the same --civitai-* token contract:

  • dist/tokens.css — a :root + [data-theme='light'|'dark'] stylesheet (--civitai-* custom properties; <color> tokens registered via @property).
  • typed JS — import { tokens, darkTokens, tokenVars, tokensCss } from '@civitai/theme'.
  • dist/tokens.dtcg.json — a W3C Design Tokens Community Group export ($value/$type/$description) for interop with token tooling.

Why generated (not hand-authored)

The tokens are produced by feeding a vendored copy of civitai's createTheme override (src/theme.source.ts) through Mantine's public pipeline — mergeMantineTheme → defaultCssVariablesResolver → transitive var() resolution → re-namespaced --civitai-*. This is the same primitive civitai uses in mantine-css-variables.ts. --mantine-* is fully resolved away, so the --civitai-* contract is self-contained.

Two kinds of token depart from that. The breakpoint scale bypasses the Mantine pipeline entirely (see below), and a few tokens declare an explicit light/dark pair — see next section.

Four guards keep it honest (pnpm --filter @civitai/theme test):

  • drift guard — reads civitai/civitai's live ThemeProvider.tsx and fails if the vendored palette/white/black diverge (set CIVITAI_REPO; skips with a clear message when the checkout is absent).
  • breakpoint drift guard — same, against civitai's src/utils/breakpoints.json.
  • generation parity — the committed generated source + built artifacts must byte-match a fresh generation, so a stale hand-edit can't slip through.
  • px-not-em guard — self-contained; pins the breakpoint tokens to the px scale and asserts the em values are absent.

Scheme-paired tokens

A custom property inherits across a shadow boundary; an ancestor selector does not cross one. So a component that wanted "gray in light, surface in dark" could only say it as [data-theme='dark'] <descendant> in @civitai/components, which a shadow root can never match (:host-context() has never shipped in Firefox).

Those decisions are tokens now — --civitai-card-border-width, --civitai-color-track, --civitai-color-segmented-bg and --civitai-color-media-placeholder. Mantine has no variable that carries either side of these pairs, so TokenSpec.source and .literal each accept a { light, dark } pair, resolved against its own scheme's variable map.

--civitai-card-border-width is a width, not a color: dark drops the default hairline's box, and a transparent color would still occupy 1px on every card.

Breakpoints — 🔴 the px scale, not Mantine's em scale

civitai has two breakpoint scales and they agree on exactly one key:

scalexssmmdlgxl
px — src/utils/breakpoints.json, mirrored by Tailwind and mantineContainerSizes. This package emits this one.480768102411841440
Mantine's stock em scale — never overridden in civitai; what every Mantine responsive prop uses57676899212001408

Only sm matches, so a wrong implementation looks correct at a glance and a test that pins sm alone passes against the wrong scale. The px scale is therefore vendored in its own module (src/breakpoints.source.ts), guarded directly against breakpoints.json, and emitted as a literal token spec that never touches mergeMantineTheme — routing it through the Mantine resolver is exactly how an un-overridden key would silently come back as the em value.

import { breakpoints, BREAKPOINT_KEYS, tokens } from '@civitai/theme';
breakpoints.md;          // 1024   ← use this for a JS width comparison
tokens.bpMd;             // "1024px"
// CSS: var(--civitai-bp-md)

Use the numeric breakpoints for width comparisons: a CSS custom property cannot appear inside a @media/@container condition, so --civitai-bp-* is for lengths (max-width, grid-template-columns), not for conditions. React blocks should use useBlockBreakpoint() from @civitai/blocks-react, which resolves the block's own measured width against this scale.

Usage

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@civitai/theme/styles.css" />

Unversioned on purpose: it tracks the latest dist-tag, so the tokens you load always include the ones documented above. A pinned URL keeps returning 200 with an old stylesheet — @0.2.0, the version this README shipped until now, carries zero --civitai-bp-* tokens, so every var(--civitai-bp-md) on this page resolved to nothing. Pin only if you need a reproducible build, and re-check the pin when you upgrade.

import { injectTokens, tokens } from '@civitai/theme';
injectTokens();          // inject the stylesheet at runtime (JS consumers)
tokens.colorPrimary;     // "#228BE6"

Theme by setting data-theme="light" | "dark" on any ancestor.

Dark is the base, and nothing here consults the OS. :root carries the dark palette plus color-scheme: dark, so an element with no data-theme above it is dark — on any machine, under any OS preference. [data-theme='light'] is a full mirror of the light palette and is the only thing that puts light back; [data-theme='dark'] restores dark inside a light subtree. The attribute is the override and it wins wherever it sits, because a nearer ancestor's tokens inherit over a farther one's.

Recolouring a token (brand overrides)

🔴 Put a token override on a SCOPE, never on :root. This sheet carries no cascade layer, and its :root / [data-theme='…'] blocks sit at specificity 0-1-0 — so a consumer's :root { --civitai-color-primary: … } does not outrank them, it ties, and the last stylesheet in the document wins.

.my-block { --civitai-color-primary: #a259ff; }   /* ✅ wins in either order */
:root     { --civitai-color-primary: #a259ff; }   /* ⚠️ wins only if this sheet loaded FIRST */

The same goes for an inline style="--civitai-color-primary: …", and a scoped value reaches inside component shadow roots, because custom properties cross the boundary.

⚠️ The :root form fails silently in the order the framework produces. @civitai/blocks-react's useBlocksStyles() calls injectTokens() from a useEffect, so these tokens are appended after a bundler-injected app stylesheet. Measured in Chromium: a :root override in that order resolves to this theme's own value with no error — with or without data-theme present, so the theme attribute is not the cause. The full order × route matrix is pinned in @civitai/components' test/token-override-order.browser.test.ts.

What you cannot do: define a theme of your own. This package exports token values and injectTokens(doc?); the generator is internal, and only the literal strings light and dark select a token block — data-theme="mybrand" selects none and inherits the dark base. Recolour the token set; there is no named-theme API.

There is deliberately no @media (prefers-color-scheme: …) block, in either direction. The browser must not decide a Civitai surface's theme: civitai.com is dark, and an App Block boots dark and takes light only from its host. ⚠️ Until 0.5.0 the opposite was true — :root was light and dark arrived via @media (prefers-color-scheme: dark) { :root:not([data-theme]) { … } } — so every surface that must not follow the OS had to set data-theme explicitly just to defeat it. If you added such an attribute for that reason alone, you can drop it.

Note on the JS exports, which did not change: tokens is still the LIGHT map and darkTokens the dark one. So tokens no longer describes what :root emits — read it as "the [data-theme='light'] values". That naming is kept deliberately, so this release breaks one thing (the CSS default) rather than two.

Build

pnpm --filter @civitai/theme build regenerates src/tokens.generated.ts, dist/tokens.css, and dist/tokens.dtcg.json, then compiles with tsc.

版本与发布记录

@civitai/theme 共发布 12 个版本,首次发布于 2026/07/23;过去 12 个月发布了 12 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.5.2latest2026/10/041 个月前
0.5.12026/10/041 个月前
0.5.02026/10/031 个月前
0.4.02026/09/231 个月前
0.3.22026/09/231 个月前
0.3.12026/09/042 个月前

关于 @civitai/theme 的常见问题

@civitai/theme 是什么?

Framework-agnostic --civitai-* design tokens, derived build-time from civitai's real Mantine theme. CSS + typed JS + W3C DTCG.

如何安装 @civitai/theme?

在项目目录运行 npm install @civitai/theme,或使用 pnpm add / yarn add / bun add。

@civitai/theme 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@civitai/theme 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@civitai/theme 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@civitai/theme 可以商用吗?

@civitai/theme 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@civitai/theme 的下载量有多少?

根据 npm 官方下载统计,@civitai/theme 在 2026/10/10 – 2026/11/08 这 30 天内被下载 17,179 次,日均约 573 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@civitai/theme 还在维护吗?

最新版本 0.5.2 发布于 2026/10/04(1 个月前);至今共发布 12 个版本,其中过去 12 个月发布了 12 个;首次发布于 2026/07/23。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。