Color mode

This page is part of Themes in Depth and covers stable CSS hooks and the cascade.

Stable CSS hooks and the cascade

3-5. userCss

userCss loads last (top of the cascade) and is the final override. It is for sites that want to tweak one thing without forking the theme. Theme stylesheets load before userCss, so userCss wins. A theme package should not rely on stronger selectors or heavy use of !important to beat userCss.

ts
theme: defaultTheme({
  userCss: ["/extensions/custom.css"],
}),

5. Stable CSS hooks

Themes target documented stable hooks, not internal markup. There are two class namespaces.

  • rb-* — structural hooks and semantic design tokens provided by the framework. Structural hooks: .rb-theme-root (the theme root container), .rb-site, .rb-article, .rb-article-layout, .rb-article-header, .rb-article-body, .rb-article-content, .rb-article-meta, .rb-article-footer, .rb-sidebar.
  • rr-<feature> — the root hook on the outermost element rendered by a plugin or feature. Examples: .rr-search, .rr-callout, .rr-table-of-contents, .rr-backlinks, .rr-local-graph, .rr-code, .rr-code-tabs, .rr-lightbox, .rr-excalidraw, .rr-mermaid, .rr-query, .rr-cardlink, .rr-diff-history, .rr-attachment, .rr-media, .rr-recent-posts, .rr-garden-explorer.

A theme should style only these root hooks and the descendants a plugin documents. BEM elements (__…) and modifiers (--…) are internal implementation details. For example, a plugin may internally use:

text
.rr-search
.rr-search__input
.rr-search__result
.rr-search--loading

The public hook is .rr-search; __input, __result, and --loading are treated as internal implementation unless the plugin explicitly documents them as public hooks. Generic helpers such as .sr-only are not plugin hooks. Plugins keep legacy classes for backward compatibility, so the same element can carry both .rr-<feature> and the old class; target rr-* from themes.

5-1. Character layer

A theme is not limited to tokens. Within the theme boundary it may style the stable hooks directly to give a site a visual character.

  • Put token definitions inside @layer base; put visual character rules unlayered. The app's structural CSS and plugin CSS are unlayered, so unlayered theme rules win over them without !important. Never use !important.
  • Target only stable hooks: .rb-site, .rb-article, .rb-article-layout, .rb-article-header, .rb-article-body, .rb-article-content, .rb-article-meta, .rb-article-footer, .rb-sidebar, and the rr-* plugin roots above. Do not invent new rb-* / rr-* class names; .rr-* BEM parts are internal.
  • .rb-article-content is where the Markdown semantic baseline lives: the structural rules that keep Markdown readable after the CSS reset (list markers and indentation, headings, paragraph and block spacing, tables, figures, definitions, inline code, and preformatted blocks). A theme styles the appearance of that baseline through --rb-* tokens and character rules; it does not need to re-declare the structure. The baseline is layered, so a theme's unlayered character rules and utility classes both win over it.
  • .rb-article-body is the article body shell that hosts the header, metadata, rendered Markdown, and plugin slots. It carries no Markdown typography itself, so plugin components keep their own headings wherever they are placed.
  • A theme may ship self-hosted webfonts (Latin subsets) in its package under styles/fonts/, reference them with relative url(), and include the font license file. Japanese and other CJK text should fall back to system font stacks rather than shipping large font files.
text
styles/
├─ theme.css
└─ fonts/
   └─ example-serif-latin.woff2
css
/* Tokens stay layered. */
@layer base {
  :is(:root, .rb-theme-root)[data-theme-name="example"] {
    --rb-color-accent: #b45309;
  }
}
 
/* Character rules are unlayered, so they beat app and plugin CSS. */
:is(:root, .rb-theme-root)[data-theme-name="example"] .rb-article-header {
  border-bottom: var(--rb-rule-width) solid var(--rb-color-border);
}
 
@font-face {
  font-family: "Example Serif";
  src: url("./fonts/example-serif-latin.woff2") format("woff2");
  font-weight: 400 700;
  font-display: swap;
}

Character rules are still presentation-only: they must not change content, structure, or behavior.

6. CSS cascade

Load order is a key contract for presentation extensions.

text
framework structural CSS
→ base / app structural CSS
→ Plugin default CSS
→ Theme CSS
→ config token inline style
→ userCss

This order is guaranteed, not incidental. @riebeckite/honox generates .riebeckite/framework-styles.css (framework structural CSS), .riebeckite/plugin-styles.css (plugin styles in resolved plugin order), and .riebeckite/theme-styles.css (theme styles). The site imports the framework stylesheet before the plugin stylesheet, and the plugin stylesheet before the theme stylesheet, so theme CSS always overrides plugin defaults and userCss is the final override.

Diagram source
text
flowchart TD
    Base["Base / Application<br/>Structural CSS"]
    Plugin["Plugin Default CSS"]
    Theme["Theme CSS"]
    Token["Config Token<br/>Inline Style"]
    User["userCss"]
 
    Base --> Plugin
    Plugin --> Theme
    Theme --> Token
    Token --> User

The resulting relationship is:

text
Plugin
  → standard appearance
 
Theme
  → changes the plugin's appearance
 
userCss
  → the site author's final adjustment

Do not reorder these imports or edit the generated files. Each generated file notes its cascade position in a header comment. The cascade usually does not depend on !important.

Troubleshooting: the theme is not applied

If a theme does not apply as expected, check in this order:

Diagram source
text
flowchart TD
    Start["Theme is not applied"]
 
    Start --> Name{"Is data-theme-name correct?"}
    Name -->|No| FixName["Check the theme name"]
    Name -->|Yes| Hook{"Are you targeting the right hook?"}
 
    Hook -->|No| FixHook["Check rb-* / rr-*"]
    Hook -->|Yes| Cascade{"Is the cascade correct?"}
 
    Cascade -->|No| FixCascade["Check Plugin → Theme → userCss"]
    Cascade -->|Yes| Mode{"Is the color mode condition correct?"}
 
    Mode -->|No| FixMode["Check data-theme / system"]
    Mode -->|Yes| CSS["Check the selector / CSS"]

In particular, verify:

  1. data-theme-name matches the theme's name.
  2. You target the correct .rb-* / .rr-* stable hook.
  3. The order is Plugin CSS → Theme CSS → userCss.
  4. data-theme="" is not left behind when using system.
  5. The selector does not leak outside the theme root.

History

1 changesCollapseExpand
1 + ---
2 + title: Stable CSS hooks and the cascade
3 + sidebar:
4 + label: Stable CSS hooks and the cascade
5 + order: 50
6 + ---
7 +
8 + This page is part of [Themes in Depth](../theme-system.md) and covers stable CSS hooks and the cascade.
9 +
10 + # Stable CSS hooks and the cascade
11 +
12 + ## 3-5. userCss
13 +
14 + `userCss` loads last (top of the cascade) and is the final override. It is for sites that want to tweak one thing without forking the theme. Theme stylesheets load before `userCss`, so `userCss` wins. A theme package should not rely on stronger selectors or heavy use of `!important` to beat `userCss`.
15 +
16 + ```ts
17 + theme: defaultTheme({
18 + userCss: ["/extensions/custom.css"],
19 + }),
20 + ```
21 +
22 + ## 5. Stable CSS hooks
23 +
24 + Themes target documented stable hooks, not internal markup. There are two class namespaces.
25 +
26 + - **`rb-*`** — structural hooks and semantic design tokens provided by the framework. Structural hooks: `.rb-theme-root` (the theme root container), `.rb-site`, `.rb-article`, `.rb-article-layout`, `.rb-article-header`, `.rb-article-body`, `.rb-article-content`, `.rb-article-meta`, `.rb-article-footer`, `.rb-sidebar`.
27 + - **`rr-<feature>`** — the root hook on the outermost element rendered by a plugin or feature. Examples: `.rr-search`, `.rr-callout`, `.rr-table-of-contents`, `.rr-backlinks`, `.rr-local-graph`, `.rr-code`, `.rr-code-tabs`, `.rr-lightbox`, `.rr-excalidraw`, `.rr-mermaid`, `.rr-query`, `.rr-cardlink`, `.rr-diff-history`, `.rr-attachment`, `.rr-media`, `.rr-recent-posts`, `.rr-garden-explorer`.
28 +
29 + A theme should style only these root hooks and the descendants a plugin documents. BEM elements (`__…`) and modifiers (`--…`) are internal implementation details. For example, a plugin may internally use:
30 +
31 + ```text
32 + .rr-search
33 + .rr-search__input
34 + .rr-search__result
35 + .rr-search--loading
36 + ```
37 +
38 + The public hook is `.rr-search`; `__input`, `__result`, and `--loading` are treated as internal implementation unless the plugin explicitly documents them as public hooks. Generic helpers such as `.sr-only` are not plugin hooks. Plugins keep legacy classes for backward compatibility, so the same element can carry both `.rr-<feature>` and the old class; target `rr-*` from themes.
39 +
40 + ### 5-1. Character layer
41 +
42 + A theme is not limited to tokens. Within the theme boundary it may style the
43 + stable hooks directly to give a site a visual character.
44 +
45 + - Put token definitions inside `@layer base`; put visual character rules
46 + **unlayered**. The app's structural CSS and plugin CSS are unlayered, so
47 + unlayered theme rules win over them without `!important`. Never use
48 + `!important`.
49 + - Target only stable hooks: `.rb-site`, `.rb-article`, `.rb-article-layout`,
50 + `.rb-article-header`, `.rb-article-body`, `.rb-article-content`,
51 + `.rb-article-meta`, `.rb-article-footer`, `.rb-sidebar`, and the `rr-*` plugin
52 + roots above. Do not invent new `rb-*` / `rr-*` class names; `.rr-*` BEM parts
53 + are internal.
54 + - `.rb-article-content` is where the Markdown semantic baseline lives: the
55 + structural rules that keep Markdown readable after the CSS reset (list
56 + markers and indentation, headings, paragraph and block spacing, tables,
57 + figures, definitions, inline code, and preformatted blocks). A theme styles
58 + the appearance of that baseline through `--rb-*` tokens and character rules;
59 + it does not need to re-declare the structure. The baseline is layered, so a
60 + theme's unlayered character rules and utility classes both win over it.
61 + - `.rb-article-body` is the article body shell that hosts the header, metadata,
62 + rendered Markdown, and plugin slots. It carries no Markdown typography itself,
63 + so plugin components keep their own headings wherever they are placed.
64 + - A theme may ship self-hosted webfonts (Latin subsets) in its package under
65 + `styles/fonts/`, reference them with relative `url()`, and include the font
66 + license file. Japanese and other CJK text should fall back to system font
67 + stacks rather than shipping large font files.
68 +
69 + ```text
70 + styles/
71 + ├─ theme.css
72 + └─ fonts/
73 + └─ example-serif-latin.woff2
74 + ```
75 +
76 + ```css
77 + /* Tokens stay layered. */
78 + @layer base {
79 + :is(:root, .rb-theme-root)[data-theme-name="example"] {
80 + --rb-color-accent: #b45309;
81 + }
82 + }
83 +
84 + /* Character rules are unlayered, so they beat app and plugin CSS. */
85 + :is(:root, .rb-theme-root)[data-theme-name="example"] .rb-article-header {
86 + border-bottom: var(--rb-rule-width) solid var(--rb-color-border);
87 + }
88 +
89 + @font-face {
90 + font-family: "Example Serif";
91 + src: url("./fonts/example-serif-latin.woff2") format("woff2");
92 + font-weight: 400 700;
93 + font-display: swap;
94 + }
95 + ```
96 +
97 + Character rules are still presentation-only: they must not change content,
98 + structure, or behavior.
99 +
100 + ## 6. CSS cascade
101 +
102 + Load order is a key contract for presentation extensions.
103 +
104 + ```text
105 + framework structural CSS
106 + → base / app structural CSS
107 + → Plugin default CSS
108 + → Theme CSS
109 + → config token inline style
110 + → userCss
111 + ```
112 +
113 + This order is guaranteed, not incidental. `@riebeckite/honox` generates `.riebeckite/framework-styles.css` (framework structural CSS), `.riebeckite/plugin-styles.css` (plugin styles in resolved plugin order), and `.riebeckite/theme-styles.css` (theme styles). The site imports the framework stylesheet before the plugin stylesheet, and the plugin stylesheet before the theme stylesheet, so theme CSS always overrides plugin defaults and `userCss` is the final override.
114 +
115 + ```mermaid
116 + flowchart TD
117 + Base["Base / Application<br/>Structural CSS"]
118 + Plugin["Plugin Default CSS"]
119 + Theme["Theme CSS"]
120 + Token["Config Token<br/>Inline Style"]
121 + User["userCss"]
122 +
123 + Base --> Plugin
124 + Plugin --> Theme
125 + Theme --> Token
126 + Token --> User
127 + ```
128 +
129 + The resulting relationship is:
130 +
131 + ```text
132 + Plugin
133 + → standard appearance
134 +
135 + Theme
136 + → changes the plugin's appearance
137 +
138 + userCss
139 + → the site author's final adjustment
140 + ```
141 +
142 + Do not reorder these imports or edit the generated files. Each generated file notes its cascade position in a header comment. The cascade usually does not depend on `!important`.
143 +
144 + ## Troubleshooting: the theme is not applied
145 +
146 + If a theme does not apply as expected, check in this order:
147 +
148 + ```mermaid
149 + flowchart TD
150 + Start["Theme is not applied"]
151 +
152 + Start --> Name{"Is data-theme-name correct?"}
153 + Name -->|No| FixName["Check the theme name"]
154 + Name -->|Yes| Hook{"Are you targeting the right hook?"}
155 +
156 + Hook -->|No| FixHook["Check rb-* / rr-*"]
157 + Hook -->|Yes| Cascade{"Is the cascade correct?"}
158 +
159 + Cascade -->|No| FixCascade["Check Plugin → Theme → userCss"]
160 + Cascade -->|Yes| Mode{"Is the color mode condition correct?"}
161 +
162 + Mode -->|No| FixMode["Check data-theme / system"]
163 + Mode -->|Yes| CSS["Check the selector / CSS"]
164 + ```
165 +
166 + In particular, verify:
167 +
168 + 1. `data-theme-name` matches the theme's `name`.
169 + 2. You target the correct `.rb-*` / `.rr-*` stable hook.
170 + 3. The order is Plugin CSS → Theme CSS → `userCss`.
171 + 4. `data-theme=""` is not left behind when using `system`.
172 + 5. The selector does not leak outside the theme root.
173 +