Color mode

Your First Theme

A theme changes how a site looks (colors, typography, layout). It cannot add features. For features, see Your first plugin. A theme must also work for Page Types it does not know: style stable hooks and semantic tokens, not a list of route or Page Type IDs.

Go in this order: adjust a built-in theme first, then build your own.

1. Adjust a built-in theme (fastest)

If you want to tweak an existing look, pass options and CSS to defaultTheme():

ts
// riebeckite.config.ts
import { defaultTheme } from "@riebeckite/theme-default";
 
export default defineConfig({
  theme: defaultTheme({
    colorMode: "light",      // "light" | "dark" | "system"
    typography: "system",    // "system" | "serif" | "sans"
    userCss: ["/extensions/custom.css"], // loaded last, overrides everything
  }),
  // ...
});

2. Create a minimal theme

A custom theme is made with defineTheme (from @riebeckite/core). It does not need to be a published package — you can define it inside the site.

ts
// extensions/local-theme.ts
import { defineTheme } from "@riebeckite/core";
 
export function localTheme() {
  return defineTheme({
    name: "local",
    styles: [{ moduleSpecifier: "/extensions/theme.css" }],
  });
}

Pass it to theme in riebeckite.config.ts.

ts
// riebeckite.config.ts
import { localTheme } from "./extensions/local-theme";
 
export default defineConfig({
  theme: localTheme(),
  // ...
});
  • name identifies the theme.
  • styles declares the stylesheet. For an in-site theme, use a module specifier the host bundler resolves, such as /extensions/theme.css.

3. Write the CSS

Do not hardcode colors. Use semantic tokens (--rb-*) and stable hooks (rb-* / rr-<feature>) so themes stay swappable, and scope every rule to the theme root selector. Replace <name> with the theme's identity name — here the theme is named local.

css
/* Example: adjust background, text color, and article width */
:is(:root, .rb-theme-root)[data-theme-name="local"] .rb-site {
  background: var(--rb-color-paper);
  color: var(--rb-color-ink);
}
 
:is(:root, .rb-theme-root)[data-theme-name="local"] .rb-article {
  max-width: var(--rb-layout-article-max);
}

The theme root selector :is(:root, .rb-theme-root)[data-theme-name="<name>"] matches the document root on a real site (the app sets data-theme-name on <html>) and any class="rb-theme-root" data-theme-name="<name>" container in a preview.

Themes also own the visual accessibility contract. Keep visible focus styles, readable contrast in light and dark modes, recognizable links, scalable text, reduced-motion behavior, and non-color-only state cues. See Accessibility.

Support color modes with these three states:

css
:is(:root, .rb-theme-root)[data-theme-name="local"] { /* light */ }
:is(:root, .rb-theme-root)[data-theme-name="local"][data-theme="dark"] { /* dark */ }
@media (prefers-color-scheme: dark) {
  :is(:root, .rb-theme-root)[data-theme-name="local"]:not([data-theme]) { /* follows the OS (system) */ }
}

CSS ordering is fixed: theme CSS loads before userCss (which is highest priority). See Theme System for the token and hook lists and the cascade details.

4. Package it for distribution (optional)

Once it works in a site, you can distribute it. Use packages/themes/minimal as a template.

text
packages/themes/minimal/
├─ src/index.ts      ← factory that calls defineTheme
├─ styles/theme.css  ← the theme stylesheet
├─ package.json      ← exports ./style.css
├─ README_ja.md
└─ README.md
  • A distributed theme depends only on @riebeckite/core and exports its stylesheet as ./style.css. Never reference monorepo paths.
  • Resolve theme-specific options inside the theme; do not grow the Core ThemeConfig.

5. Verify

sh
npm exec riebeckite check             # validate config and plugin resolution
npm exec riebeckite inspect config# inspect the resolved theme
npm exec riebeckite dev               # check the look locally
npm exec riebeckite build             # check the generated output

check / doctor / inspect are read-only. Fix what the diagnostics say.

Further reading

History

1 changesCollapseExpand
1 + # Your First Theme
2 +
3 + A theme changes how a site **looks** (colors, typography, layout). It cannot add features. For features, see [Your first plugin](../plugins/writing-a-plugin.en.md). A theme must also work for Page Types it does not know: style stable hooks and semantic tokens, not a list of route or Page Type IDs.
4 +
5 + Go in this order: adjust a built-in theme first, then build your own.
6 +
7 + ## 1. Adjust a built-in theme (fastest)
8 +
9 + If you want to tweak an existing look, pass options and CSS to `defaultTheme()`:
10 +
11 + ```ts
12 + // riebeckite.config.ts
13 + import { defaultTheme } from "@riebeckite/theme-default";
14 +
15 + export default defineConfig({
16 + theme: defaultTheme({
17 + colorMode: "light", // "light" | "dark" | "system"
18 + typography: "system", // "system" | "serif" | "sans"
19 + userCss: ["/extensions/custom.css"], // loaded last, overrides everything
20 + }),
21 + // ...
22 + });
23 + ```
24 +
25 + ## 2. Create a minimal theme
26 +
27 + A custom theme is made with `defineTheme` (from `@riebeckite/core`). It does **not** need to be a published package — you can define it inside the site.
28 +
29 + ```ts
30 + // extensions/local-theme.ts
31 + import { defineTheme } from "@riebeckite/core";
32 +
33 + export function localTheme() {
34 + return defineTheme({
35 + name: "local",
36 + styles: [{ moduleSpecifier: "/extensions/theme.css" }],
37 + });
38 + }
39 + ```
40 +
41 + Pass it to `theme` in `riebeckite.config.ts`.
42 +
43 + ```ts
44 + // riebeckite.config.ts
45 + import { localTheme } from "./extensions/local-theme";
46 +
47 + export default defineConfig({
48 + theme: localTheme(),
49 + // ...
50 + });
51 + ```
52 +
53 + - `name` identifies the theme.
54 + - `styles` declares the stylesheet. For an in-site theme, use a module specifier the host bundler resolves, such as `/extensions/theme.css`.
55 +
56 + ## 3. Write the CSS
57 +
58 + Do not hardcode colors. Use **semantic tokens (`--rb-*`) and stable hooks (`rb-*` / `rr-<feature>`)** so themes stay swappable, and scope every rule to the theme root selector. Replace `<name>` with the theme's identity name — here the theme is named `local`.
59 +
60 + ```css
61 + /* Example: adjust background, text color, and article width */
62 + :is(:root, .rb-theme-root)[data-theme-name="local"] .rb-site {
63 + background: var(--rb-color-paper);
64 + color: var(--rb-color-ink);
65 + }
66 +
67 + :is(:root, .rb-theme-root)[data-theme-name="local"] .rb-article {
68 + max-width: var(--rb-layout-article-max);
69 + }
70 + ```
71 +
72 + The theme root selector `:is(:root, .rb-theme-root)[data-theme-name="<name>"]` matches the document root on a real site (the app sets `data-theme-name` on `<html>`) and any `class="rb-theme-root" data-theme-name="<name>"` container in a preview.
73 +
74 + Themes also own the visual accessibility contract. Keep visible focus styles, readable contrast in light and dark modes, recognizable links, scalable text, reduced-motion behavior, and non-color-only state cues. See [Accessibility](../accessibility.en.md).
75 +
76 + Support color modes with these three states:
77 +
78 + ```css
79 + :is(:root, .rb-theme-root)[data-theme-name="local"] { /* light */ }
80 + :is(:root, .rb-theme-root)[data-theme-name="local"][data-theme="dark"] { /* dark */ }
81 + @media (prefers-color-scheme: dark) {
82 + :is(:root, .rb-theme-root)[data-theme-name="local"]:not([data-theme]) { /* follows the OS (system) */ }
83 + }
84 + ```
85 +
86 + CSS ordering is fixed: theme CSS loads before `userCss` (which is highest priority). See [Theme System](../reference/theme-api.en.md) for the token and hook lists and the cascade details.
87 +
88 + ## 4. Package it for distribution (optional)
89 +
90 + Once it works in a site, you can distribute it. Use `packages/themes/minimal` as a template.
91 +
92 + ```text
93 + packages/themes/minimal/
94 + ├─ src/index.ts ← factory that calls defineTheme
95 + ├─ styles/theme.css ← the theme stylesheet
96 + ├─ package.json ← exports ./style.css
97 + ├─ README_ja.md
98 + └─ README.md
99 + ```
100 +
101 + - A distributed theme depends only on `@riebeckite/core` and exports its stylesheet as `./style.css`. Never reference monorepo paths.
102 + - Resolve theme-specific options inside the theme; do not grow the Core `ThemeConfig`.
103 +
104 + ## 5. Verify
105 +
106 + ```sh
107 + npm exec riebeckite check # validate config and plugin resolution
108 + npm exec riebeckite inspect config# inspect the resolved theme
109 + npm exec riebeckite dev # check the look locally
110 + npm exec riebeckite build # check the generated output
111 + ```
112 +
113 + `check` / `doctor` / `inspect` are read-only. Fix what the diagnostics say.
114 +
115 + ## Further reading
116 +
117 + - [Themes in depth](../framework/theme-system.en.md) — the in-depth companion (options, tokens, hooks, cascade, packaging)
118 + - [Theme System](../reference/theme-api.en.md) — theme contract, tokens, hooks, cascade
119 + - [Plugin System](../reference/plugin-api.en.md) — the boundary with themes (features = plugins)
120 + - [Framework Reference](../reference/README.en.md) — public APIs like `defineTheme`
121 +