Color mode

はじめてのテーマ作成

Riebeckite の Theme は、Site の見た目を変更する仕組みです。

たとえば、

  • 配色
  • フォント
  • 文字サイズ
  • 余白
  • 記事幅
  • Layout
  • Light / Dark Mode

などを変更できます。

一方で、Theme から新しい機能を追加することはできません。

Diagram source
text
flowchart TD
    Want{"何を作りたい?"}
 
    Want -->|"見た目を変える"| Theme["Theme"]
    Want -->|"機能を追加する"| Plugin["Plugin"]
 
    Theme --> Visual["Color / Font / Layout / Spacing"]
    Plugin --> Feature["Search / Mermaid / Analytics / Page"]

検索、図表、Markdown の拡張などを追加したい場合は、はじめてのプラグイン作成 を参照してください。

この Guide では、

text
既存Themeを少し調整
        ↓
Site内にThemeを作る
        ↓
CSSを書く
        ↓
Light / Darkに対応
        ↓
必要ならPackageとして配布

の順に進めます。

まずは Theme を作る必要があるか確認する

少しだけ見た目を変えたい場合は、新しい Theme を作る必要はありません。

たとえば、

text
記事幅を少し変えたい
文字サイズを調整したい
Site固有のCSSを追加したい

程度なら、既存 Theme の userCss を利用できます。

一方、

text
独自の配色を作りたい
Typographyを一式設計したい
複数Siteで再利用したい
他の利用者へ配布したい

場合は Theme として作るのが向いています。

Diagram source
text
flowchart TD
    Change{"どの程度変更する?"}
 
    Change -->|"小さなSite固有調整"| UserCss["userCss"]
    Change -->|"まとまったDesign"| Theme["独自Theme"]
    Change -->|"機能も追加したい"| Plugin["Plugin"]

1. 組み込み Theme を調整する

最も簡単なのは、既存 Theme に Option と userCss を指定する方法です。

たとえば Default Theme を調整します。

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

ここでは、

text
colorMode
  → light
 
typography
  → system
 
userCss
  → /extensions/custom.css

を指定しています。

userCss は Theme の CSS より後に読み込まれるため、Site 固有の小さな上書きに利用できます。

たとえば、

css
.rb-article {
  font-size: 1.05rem;
}

のような調整ができます。

これだけで目的を達成できるなら、独自 Theme を作る必要はありません。

2. 最小の Theme を作る

独自 Theme は defineTheme で定義します。

defineTheme は @riebeckite/core から Import します。

最初から npm Package を作る必要はありません。

まずは Site の中に、

text
my-site/
├─ extensions/
│  ├─ local-theme.ts
│  └─ theme.css
│
├─ riebeckite.config.ts
└─ package.json

のように置いて作れます。

Theme を定義する

extensions/local-theme.ts を作ります。

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

最小構成では、

text
name
  → Themeの識別子
 
styles
  → Themeが使うStylesheet

を指定します。

styles[].moduleSpecifier には、Host Bundler が解決できる Module Specifier を指定します。

Site 内 Theme なら、

text
/extensions/theme.css

のように指定できます。

3. Site で Theme を使う

作成した localTheme を riebeckite.config.ts から読み込みます。

ts
// riebeckite.config.ts
import { defineConfig } from "@riebeckite/core";
import { localTheme } from "./extensions/local-theme";
 
export default defineConfig({
  theme: localTheme(),
 
  // ...
});

これで、

text
local-theme.ts
      ↓
localTheme()
      ↓
defineTheme()
      ↓
riebeckite.config.ts
      ↓
Site

という形で独自 Theme が利用されます。

4. Theme の CSS を書く

次に、

text
extensions/theme.css

へ実際の Style を書きます。

Theme の CSS では、Riebeckite が提供する、

  • Semantic Token
  • Stable CSS Hook

を利用します。

最小の例は次のようになります。

css
: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);
}

最初は少し長く見えますが、それぞれに役割があります。

text
[data-theme-name="local"]
  → このThemeだけに適用する
 
.rb-site / .rb-article
  → Riebeckiteの安定したCSS Hook
 
--rb-*
  → RiebeckiteのSemantic Token

Theme Root Selector

Theme の Style は、Theme Root の内側に限定します。

基本形は、

css
:is(:root, .rb-theme-root)[data-theme-name="<name>"]

です。

<name> には defineTheme で指定した name を入れます。

今回なら、

ts
name: "local"

なので、

css
:is(:root, .rb-theme-root)[data-theme-name="local"]

となります。

なぜ :root と .rb-theme-root の両方がある?

この Selector は、実際の Site と Theme Preview の両方で同じ CSS を利用するためのものです。

Diagram source
text
flowchart TD
    CSS["Theme CSS"]
 
    CSS --> Root[":root<br/>実際のSite"]
    CSS --> Preview[".rb-theme-root<br/>Theme Preview"]

実際の Site では、Application が Document Root に、

html
<html data-theme-name="local">

のような Theme 情報を付けます。

Theme Preview では、

html
<div
  class="rb-theme-root"
  data-theme-name="local"
>

のような任意の Container 内で Theme を表示できます。

そのため Theme CSS は、

css
:is(:root, .rb-theme-root)[data-theme-name="local"]

を Root として書きます。

Semantic Token を使う

Theme では、色や Layout の値を直接あちこちへ書くのではなく、Semantic Token を利用します。

Riebeckite の共通 Token は、

text
--rb-*

という名前です。

たとえば、

css
color: var(--rb-color-ink);
background: var(--rb-color-paper);

のように利用します。

text
paper
  → 背景
 
ink
  → 主な文字
 
accent
  → 強調
 
border
  → 境界線

のように、具体的な色ではなく役割を表す Token になっています。

これが Semantic Token です。

Theme 全体で同じ意味の色を共有できるため、

text
#ffffff
#111111
#888888

のような値を各 Component に直接書き散らすより、Theme を管理しやすくなります。

Token の正確な一覧は Theme API を参照してください。

Stable Hook を使う

Riebeckite の共通 UI には、

text
rb-*

という Stable Hook があります。

たとえば、

css
.rb-site
.rb-article

などです。

Plugin が提供する UI では、

text
rr-<feature>

形式の Stable Hook を利用します。

Theme は、特定の Route や Page Type ID に依存するのではなく、こうした Stable Hook を対象に Style を書きます。

text
避ける
 
特定Route
特定Page Type ID
内部Component構造
 
        ↓
 
使う
 
rb-* Stable Hook
rr-<feature> Stable Hook
--rb-* Semantic Token

これによって、Theme を作った時点では存在していなかった Page Type にも、共通の Design を適用しやすくなります。

なぜ Page Type ごとに CSS を書かない?

Plugin は独自の Page Type を追加できます。

そのため Theme 側で、

text
home
article
explore
tags
...

のように Page Type を列挙してしまうと、新しい Plugin が Page を追加するたびに Theme の変更が必要になります。

代わりに、

text
Page Type
     ↓
Framework / PluginのStable Hook
     ↓
Theme

という関係にします。

Diagram source
text
flowchart LR
    A["既存Page"]
    B["将来追加されたPage"]
    Hooks["Stable Hooks<br/>rb-* / rr-*"]
    Theme["Theme"]
 
    A --> Hooks
    B --> Hooks
    Hooks --> Theme

Theme が Page の種類ではなく Semantic Hook を見ることで、新しい Page Type とも疎結合にできます。

5. Light / Dark Mode に対応する

Color Mode に対応する場合は、3つの状態を考えます。

text
light
dark
system

基本形は次のようになります。

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]) {
    /* system でOSがdark */
  }
}

Light

通常の Light Mode です。

css
:is(:root, .rb-theme-root)[data-theme-name="local"] {
  /* light */
}

Dark

明示的に Dark Mode が選択されている場合は、

css
:is(:root, .rb-theme-root)[data-theme-name="local"][data-theme="dark"] {
  /* dark */
}

で扱います。

System

system では data-theme を付けず、OS / Browser の設定に従います。

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

System Mode では、

html
data-theme=""

ではなく、data-theme Attribute 自体が存在しません。

そのため、

css
:not([data-theme])

で判定します。

6. アクセシビリティを確認する

Theme は見た目を変更するため、アクセシビリティにも影響します。

特に、

  • 本文と背景の Contrast
  • Link が本文と区別できるか
  • Keyboard 操作中の Focus が見えるか
  • Light Mode / Dark Mode の両方で読めるか
  • Hover だけに情報を依存していないか

などを確認してください。

Theme は単に「きれいに見える」だけではなく、Content を読みやすい状態に保つ必要があります。

詳しくは アクセシビリティ を参照してください。

7. CSS の読み込み順を理解する

Riebeckite では CSS の読み込み順が決まっています。

Theme CSS は userCss より前に適用されます。

概念的には、

text
Framework / Application
        ↓
Plugin Style
        ↓
Theme Style
        ↓
ConfigによるToken
        ↓
userCss

という順になります。

そのため、

text
Theme
  → 再利用できる基本Design
 
userCss
  → Site固有の最終調整

という役割分担ができます。

Theme 側で Site 固有の上書きまで抱え込む必要はありません。

正確な Cascade は Theme API を参照してください。

8. Browser で確認する

Theme を作ったら Development Server を起動します。

sh
npm exec riebeckite dev

実際の記事を開いて、

  • 本文
  • 見出し
  • Link
  • Code Block
  • Table
  • List
  • Image
  • Plugin UI
  • Light Mode
  • Dark Mode

などを確認します。

特定の Demo Page だけではなく、実際の記事でも確認してください。

9. Riebeckite の検証を実行する

設定に問題がないか確認します。

sh
npm exec riebeckite check
npm exec riebeckite doctor
npm exec riebeckite inspect config
npm exec riebeckite build

それぞれの役割は次のとおりです。

Command 確認すること
check Config や Theme の解決が正しいか
doctor Site 全体に問題がないか
inspect config 解決済み Theme 設定
build 実際に Site を生成できるか

check / doctor / inspect は読み取り専用です。

これらの Command が Theme File を自動で修正することはありません。

10. 配布用 Package にする

Site 内 Theme として問題なく動作したら、必要に応じて Package として配布できます。

Riebeckite Repository 内では、

text
packages/themes/minimal/

が構成例になります。

text
packages/themes/minimal/
├─ src/
│  └─ index.ts
├─ styles/
│  └─ theme.css
├─ package.json
├─ README_ja.md
└─ README.md

src/index.ts では Theme Factory を公開します。

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

Package では Stylesheet を、

text
./style.css

のような Public Export として公開します。

外部 Theme の依存関係

配布する Theme は、

text
@riebeckite/core

の Public API に依存します。

Riebeckite Monorepo 内部の、

text
packages/...
src/...
../../...

のような Path に依存させないでください。

Diagram source
text
flowchart LR
    Theme["External Theme"]
    Core["@riebeckite/core<br/>Public API"]
    Internal["Riebeckite内部Path"]
 
    Theme --> Core
    Theme -.->|"依存しない"| Internal

外部利用者が npm から Theme をインストールした場合でも動作する構成にします。

Theme 独自 Option

Theme 固有の設定が必要な場合は、その Theme の Factory Option として定義します。

たとえば、

ts
myTheme({
  // Theme固有Option
});

のような形です。

Theme 固有の都合だけで Core の共通 Config を増やすのは避けます。

text
Riebeckite全体で共通
  → Core Contract
 
そのThemeだけで必要
  → Theme Factory Option

という境界で考えます。

Theme がしてはいけないこと

Theme の責任は Presentation です。

そのため、

text
Routeを追加する
Pageを追加する
Pluginを追加・削除する
JavaScriptの機能を追加する
DOMを変換する
Islandを追加する
ContentManagerを操作する
Filesystemを読む

といった処理は Theme に入れません。

必要な責務に応じて、

Diagram source
text
flowchart TD
    Need{"何を変更する?"}
 
    Need -->|"見た目"| Theme["Theme"]
    Need -->|"再利用できる機能"| Plugin["Plugin"]
    Need -->|"Site固有のPage / 構成"| App["Application"]
    Need -->|"Framework共通Contract"| Core["Core"]

と分けます。

最小の完成形

最小の Site 内 Theme は2ファイルで作れます。

text
extensions/
├─ local-theme.ts
└─ theme.css

local-theme.ts の中身は次のとおりです。

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

theme.css の中身は次のとおりです。

css
: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);
}

そして riebeckite.config.ts から、

ts
import { localTheme } from "./extensions/local-theme";
 
export default defineConfig({
  theme: localTheme(),
});

と指定します。

これが Riebeckite Theme の最小構成です。

まとめ

Theme を作るときは、最初から Package 化する必要はありません。

text
少しだけ変更
  → userCss
 
独自Designを作る
  → Site内Theme
 
再利用・配布したい
  → Theme Package

Theme の CSS では、

text
--rb-*
  → Semantic Token
 
rb-*
  → Framework Stable Hook
 
rr-<feature>
  → Plugin Stable Hook
 
Theme Root Selector
  → ThemeのStyleを適用する範囲

を利用します。

そして、Theme が Page Type や内部 Component の構造を知りすぎないことが最も重要です。

text
Page Type
     ↓
Stable Hook / Semantic Token
     ↓
Theme

という境界を保つことで、新しい Plugin や Page Type が追加されても利用できる Theme を作れます。

関連資料

History

1 changesCollapseExpand
1 + # はじめてのテーマ作成
2 +
3 + Riebeckite の Theme は、Site の**見た目を変更する仕組み**です。
4 +
5 + たとえば、
6 +
7 + - 配色
8 + - フォント
9 + - 文字サイズ
10 + - 余白
11 + - 記事幅
12 + - Layout
13 + - Light / Dark Mode
14 +
15 + などを変更できます。
16 +
17 + 一方で、Theme から新しい機能を追加することはできません。
18 +
19 + ```mermaid id="kmfyjs"
20 + flowchart TD
21 + Want{"何を作りたい?"}
22 +
23 + Want -->|"見た目を変える"| Theme["Theme"]
24 + Want -->|"機能を追加する"| Plugin["Plugin"]
25 +
26 + Theme --> Visual["Color / Font / Layout / Spacing"]
27 + Plugin --> Feature["Search / Mermaid / Analytics / Page"]
28 + ```
29 +
30 + 検索、図表、Markdown の拡張などを追加したい場合は、[はじめてのプラグイン作成](../plugins/writing-a-plugin.ja.md) を参照してください。
31 +
32 + この Guide では、
33 +
34 + ```text id="br3nd6"
35 + 既存Themeを少し調整
36 + ↓
37 + Site内にThemeを作る
38 + ↓
39 + CSSを書く
40 + ↓
41 + Light / Darkに対応
42 + ↓
43 + 必要ならPackageとして配布
44 + ```
45 +
46 + の順に進めます。
47 +
48 + # まずは Theme を作る必要があるか確認する
49 +
50 + 少しだけ見た目を変えたい場合は、新しい Theme を作る必要はありません。
51 +
52 + たとえば、
53 +
54 + ```text id="b83ut7"
55 + 記事幅を少し変えたい
56 + 文字サイズを調整したい
57 + Site固有のCSSを追加したい
58 + ```
59 +
60 + 程度なら、既存 Theme の `userCss` を利用できます。
61 +
62 + 一方、
63 +
64 + ```text id="71uv2w"
65 + 独自の配色を作りたい
66 + Typographyを一式設計したい
67 + 複数Siteで再利用したい
68 + 他の利用者へ配布したい
69 + ```
70 +
71 + 場合は Theme として作るのが向いています。
72 +
73 + ```mermaid id="dq1mge"
74 + flowchart TD
75 + Change{"どの程度変更する?"}
76 +
77 + Change -->|"小さなSite固有調整"| UserCss["userCss"]
78 + Change -->|"まとまったDesign"| Theme["独自Theme"]
79 + Change -->|"機能も追加したい"| Plugin["Plugin"]
80 + ```
81 +
82 + # 1. 組み込み Theme を調整する
83 +
84 + 最も簡単なのは、既存 Theme に Option と `userCss` を指定する方法です。
85 +
86 + たとえば Default Theme を調整します。
87 +
88 + ```ts id="ih0etb"
89 + // riebeckite.config.ts
90 + import { defaultTheme } from "@riebeckite/theme-default";
91 +
92 + export default defineConfig({
93 + theme: defaultTheme({
94 + colorMode: "light",
95 + typography: "system",
96 + userCss: ["/extensions/custom.css"],
97 + }),
98 +
99 + // ...
100 + });
101 + ```
102 +
103 + ここでは、
104 +
105 + ```text id="14l98v"
106 + colorMode
107 + → light
108 +
109 + typography
110 + → system
111 +
112 + userCss
113 + → /extensions/custom.css
114 + ```
115 +
116 + を指定しています。
117 +
118 + `userCss` は Theme の CSS より後に読み込まれるため、Site 固有の小さな上書きに利用できます。
119 +
120 + たとえば、
121 +
122 + ```css id="q11q8v"
123 + .rb-article {
124 + font-size: 1.05rem;
125 + }
126 + ```
127 +
128 + のような調整ができます。
129 +
130 + これだけで目的を達成できるなら、独自 Theme を作る必要はありません。
131 +
132 + # 2. 最小の Theme を作る
133 +
134 + 独自 Theme は `defineTheme` で定義します。
135 +
136 + `defineTheme` は `@riebeckite/core` から Import します。
137 +
138 + 最初から npm Package を作る必要はありません。
139 +
140 + まずは Site の中に、
141 +
142 + ```text id="byvcr1"
143 + my-site/
144 + ├─ extensions/
145 + │ ├─ local-theme.ts
146 + │ └─ theme.css
147 + │
148 + ├─ riebeckite.config.ts
149 + └─ package.json
150 + ```
151 +
152 + のように置いて作れます。
153 +
154 + ## Theme を定義する
155 +
156 + `extensions/local-theme.ts` を作ります。
157 +
158 + ```ts id="bs4dkd"
159 + // extensions/local-theme.ts
160 + import { defineTheme } from "@riebeckite/core";
161 +
162 + export function localTheme() {
163 + return defineTheme({
164 + name: "local",
165 + styles: [
166 + {
167 + moduleSpecifier: "/extensions/theme.css",
168 + },
169 + ],
170 + });
171 + }
172 + ```
173 +
174 + 最小構成では、
175 +
176 + ```text id="dbwgoq"
177 + name
178 + → Themeの識別子
179 +
180 + styles
181 + → Themeが使うStylesheet
182 + ```
183 +
184 + を指定します。
185 +
186 + `styles[].moduleSpecifier` には、Host Bundler が解決できる Module Specifier を指定します。
187 +
188 + Site 内 Theme なら、
189 +
190 + ```text id="42dg5e"
191 + /extensions/theme.css
192 + ```
193 +
194 + のように指定できます。
195 +
196 + # 3. Site で Theme を使う
197 +
198 + 作成した `localTheme` を `riebeckite.config.ts` から読み込みます。
199 +
200 + ```ts id="91cdvs"
201 + // riebeckite.config.ts
202 + import { defineConfig } from "@riebeckite/core";
203 + import { localTheme } from "./extensions/local-theme";
204 +
205 + export default defineConfig({
206 + theme: localTheme(),
207 +
208 + // ...
209 + });
210 + ```
211 +
212 + これで、
213 +
214 + ```text id="kpcf2q"
215 + local-theme.ts
216 + ↓
217 + localTheme()
218 + ↓
219 + defineTheme()
220 + ↓
221 + riebeckite.config.ts
222 + ↓
223 + Site
224 + ```
225 +
226 + という形で独自 Theme が利用されます。
227 +
228 + # 4. Theme の CSS を書く
229 +
230 + 次に、
231 +
232 + ```text id="mx8gdm"
233 + extensions/theme.css
234 + ```
235 +
236 + へ実際の Style を書きます。
237 +
238 + Theme の CSS では、Riebeckite が提供する、
239 +
240 + - Semantic Token
241 + - Stable CSS Hook
242 +
243 + を利用します。
244 +
245 + 最小の例は次のようになります。
246 +
247 + ```css id="j5jkvb"
248 + :is(:root, .rb-theme-root)[data-theme-name="local"] .rb-site {
249 + background: var(--rb-color-paper);
250 + color: var(--rb-color-ink);
251 + }
252 +
253 + :is(:root, .rb-theme-root)[data-theme-name="local"] .rb-article {
254 + max-width: var(--rb-layout-article-max);
255 + }
256 + ```
257 +
258 + 最初は少し長く見えますが、それぞれに役割があります。
259 +
260 + ```text id="ehn64e"
261 + [data-theme-name="local"]
262 + → このThemeだけに適用する
263 +
264 + .rb-site / .rb-article
265 + → Riebeckiteの安定したCSS Hook
266 +
267 + --rb-*
268 + → RiebeckiteのSemantic Token
269 + ```
270 +
271 + # Theme Root Selector
272 +
273 + Theme の Style は、Theme Root の内側に限定します。
274 +
275 + 基本形は、
276 +
277 + ```css id="5t9k0r"
278 + :is(:root, .rb-theme-root)[data-theme-name="<name>"]
279 + ```
280 +
281 + です。
282 +
283 + `<name>` には `defineTheme` で指定した `name` を入れます。
284 +
285 + 今回なら、
286 +
287 + ```ts id="2ukjvx"
288 + name: "local"
289 + ```
290 +
291 + なので、
292 +
293 + ```css id="u27p4e"
294 + :is(:root, .rb-theme-root)[data-theme-name="local"]
295 + ```
296 +
297 + となります。
298 +
299 + ## なぜ `:root` と `.rb-theme-root` の両方がある?
300 +
301 + この Selector は、実際の Site と Theme Preview の両方で同じ CSS を利用するためのものです。
302 +
303 + ```mermaid id="m0r13h"
304 + flowchart TD
305 + CSS["Theme CSS"]
306 +
307 + CSS --> Root[":root<br/>実際のSite"]
308 + CSS --> Preview[".rb-theme-root<br/>Theme Preview"]
309 + ```
310 +
311 + 実際の Site では、Application が Document Root に、
312 +
313 + ```html id="tfjfs9"
314 + <html data-theme-name="local">
315 + ```
316 +
317 + のような Theme 情報を付けます。
318 +
319 + Theme Preview では、
320 +
321 + ```html id="xwskmw"
322 + <div
323 + class="rb-theme-root"
324 + data-theme-name="local"
325 + >
326 + ```
327 +
328 + のような任意の Container 内で Theme を表示できます。
329 +
330 + そのため Theme CSS は、
331 +
332 + ```css id="7xf9zn"
333 + :is(:root, .rb-theme-root)[data-theme-name="local"]
334 + ```
335 +
336 + を Root として書きます。
337 +
338 + # Semantic Token を使う
339 +
340 + Theme では、色や Layout の値を直接あちこちへ書くのではなく、Semantic Token を利用します。
341 +
342 + Riebeckite の共通 Token は、
343 +
344 + ```text id="f0yh64"
345 + --rb-*
346 + ```
347 +
348 + という名前です。
349 +
350 + たとえば、
351 +
352 + ```css id="m5h77c"
353 + color: var(--rb-color-ink);
354 + background: var(--rb-color-paper);
355 + ```
356 +
357 + のように利用します。
358 +
359 + ```text id="w2v0ss"
360 + paper
361 + → 背景
362 +
363 + ink
364 + → 主な文字
365 +
366 + accent
367 + → 強調
368 +
369 + border
370 + → 境界線
371 + ```
372 +
373 + のように、具体的な色ではなく**役割**を表す Token になっています。
374 +
375 + これが Semantic Token です。
376 +
377 + Theme 全体で同じ意味の色を共有できるため、
378 +
379 + ```text id="bcv48d"
380 + #ffffff
381 + #111111
382 + #888888
383 + ```
384 +
385 + のような値を各 Component に直接書き散らすより、Theme を管理しやすくなります。
386 +
387 + Token の正確な一覧は [Theme API](../reference/theme-api.ja.md) を参照してください。
388 +
389 + # Stable Hook を使う
390 +
391 + Riebeckite の共通 UI には、
392 +
393 + ```text id="lq35s3"
394 + rb-*
395 + ```
396 +
397 + という Stable Hook があります。
398 +
399 + たとえば、
400 +
401 + ```css id="2jhw92"
402 + .rb-site
403 + .rb-article
404 + ```
405 +
406 + などです。
407 +
408 + Plugin が提供する UI では、
409 +
410 + ```text id="qg0kb3"
411 + rr-<feature>
412 + ```
413 +
414 + 形式の Stable Hook を利用します。
415 +
416 + Theme は、特定の Route や Page Type ID に依存するのではなく、こうした Stable Hook を対象に Style を書きます。
417 +
418 + ```text id="v5o69k"
419 + 避ける
420 +
421 + 特定Route
422 + 特定Page Type ID
423 + 内部Component構造
424 +
425 + ↓
426 +
427 + 使う
428 +
429 + rb-* Stable Hook
430 + rr-<feature> Stable Hook
431 + --rb-* Semantic Token
432 + ```
433 +
434 + これによって、Theme を作った時点では存在していなかった Page Type にも、共通の Design を適用しやすくなります。
435 +
436 + # なぜ Page Type ごとに CSS を書かない?
437 +
438 + Plugin は独自の Page Type を追加できます。
439 +
440 + そのため Theme 側で、
441 +
442 + ```text id="70qh6b"
443 + home
444 + article
445 + explore
446 + tags
447 + ...
448 + ```
449 +
450 + のように Page Type を列挙してしまうと、新しい Plugin が Page を追加するたびに Theme の変更が必要になります。
451 +
452 + 代わりに、
453 +
454 + ```text id="myo7di"
455 + Page Type
456 + ↓
457 + Framework / PluginのStable Hook
458 + ↓
459 + Theme
460 + ```
461 +
462 + という関係にします。
463 +
464 + ```mermaid id="ox4v8a"
465 + flowchart LR
466 + A["既存Page"]
467 + B["将来追加されたPage"]
468 + Hooks["Stable Hooks<br/>rb-* / rr-*"]
469 + Theme["Theme"]
470 +
471 + A --> Hooks
472 + B --> Hooks
473 + Hooks --> Theme
474 + ```
475 +
476 + Theme が Page の種類ではなく Semantic Hook を見ることで、新しい Page Type とも疎結合にできます。
477 +
478 + # 5. Light / Dark Mode に対応する
479 +
480 + Color Mode に対応する場合は、3つの状態を考えます。
481 +
482 + ```text id="k24wdk"
483 + light
484 + dark
485 + system
486 + ```
487 +
488 + 基本形は次のようになります。
489 +
490 + ```css id="c9pm84"
491 + :is(:root, .rb-theme-root)[data-theme-name="local"] {
492 + /* light */
493 + }
494 +
495 + :is(:root, .rb-theme-root)[data-theme-name="local"][data-theme="dark"] {
496 + /* dark */
497 + }
498 +
499 + @media (prefers-color-scheme: dark) {
500 + :is(:root, .rb-theme-root)[data-theme-name="local"]:not([data-theme]) {
501 + /* system でOSがdark */
502 + }
503 + }
504 + ```
505 +
506 + ## Light
507 +
508 + 通常の Light Mode です。
509 +
510 + ```css id="6s0kr3"
511 + :is(:root, .rb-theme-root)[data-theme-name="local"] {
512 + /* light */
513 + }
514 + ```
515 +
516 + ## Dark
517 +
518 + 明示的に Dark Mode が選択されている場合は、
519 +
520 + ```css id="khq4ec"
521 + :is(:root, .rb-theme-root)[data-theme-name="local"][data-theme="dark"] {
522 + /* dark */
523 + }
524 + ```
525 +
526 + で扱います。
527 +
528 + ## System
529 +
530 + `system` では `data-theme` を付けず、OS / Browser の設定に従います。
531 +
532 + ```css id="4nrvxb"
533 + @media (prefers-color-scheme: dark) {
534 + :is(:root, .rb-theme-root)[data-theme-name="local"]:not([data-theme]) {
535 + /* system dark */
536 + }
537 + }
538 + ```
539 +
540 + System Mode では、
541 +
542 + ```html id="k8uz2d"
543 + data-theme=""
544 + ```
545 +
546 + ではなく、**`data-theme` Attribute 自体が存在しません**。
547 +
548 + そのため、
549 +
550 + ```css id="dh4axr"
551 + :not([data-theme])
552 + ```
553 +
554 + で判定します。
555 +
556 + # 6. アクセシビリティを確認する
557 +
558 + Theme は見た目を変更するため、アクセシビリティにも影響します。
559 +
560 + 特に、
561 +
562 + - 本文と背景の Contrast
563 + - Link が本文と区別できるか
564 + - Keyboard 操作中の Focus が見えるか
565 + - Light Mode / Dark Mode の両方で読めるか
566 + - Hover だけに情報を依存していないか
567 +
568 + などを確認してください。
569 +
570 + Theme は単に「きれいに見える」だけではなく、Content を読みやすい状態に保つ必要があります。
571 +
572 + 詳しくは [アクセシビリティ](../accessibility.ja.md) を参照してください。
573 +
574 + # 7. CSS の読み込み順を理解する
575 +
576 + Riebeckite では CSS の読み込み順が決まっています。
577 +
578 + Theme CSS は `userCss` より前に適用されます。
579 +
580 + 概念的には、
581 +
582 + ```text id="tl3h0p"
583 + Framework / Application
584 + ↓
585 + Plugin Style
586 + ↓
587 + Theme Style
588 + ↓
589 + ConfigによるToken
590 + ↓
591 + userCss
592 + ```
593 +
594 + という順になります。
595 +
596 + そのため、
597 +
598 + ```text id="8i6rvp"
599 + Theme
600 + → 再利用できる基本Design
601 +
602 + userCss
603 + → Site固有の最終調整
604 + ```
605 +
606 + という役割分担ができます。
607 +
608 + Theme 側で Site 固有の上書きまで抱え込む必要はありません。
609 +
610 + 正確な Cascade は [Theme API](../reference/theme-api.ja.md) を参照してください。
611 +
612 + # 8. Browser で確認する
613 +
614 + Theme を作ったら Development Server を起動します。
615 +
616 + ```sh id="p90emf"
617 + npm exec riebeckite dev
618 + ```
619 +
620 + 実際の記事を開いて、
621 +
622 + - 本文
623 + - 見出し
624 + - Link
625 + - Code Block
626 + - Table
627 + - List
628 + - Image
629 + - Plugin UI
630 + - Light Mode
631 + - Dark Mode
632 +
633 + などを確認します。
634 +
635 + 特定の Demo Page だけではなく、実際の記事でも確認してください。
636 +
637 + # 9. Riebeckite の検証を実行する
638 +
639 + 設定に問題がないか確認します。
640 +
641 + ```sh id="mbllje"
642 + npm exec riebeckite check
643 + npm exec riebeckite doctor
644 + npm exec riebeckite inspect config
645 + npm exec riebeckite build
646 + ```
647 +
648 + それぞれの役割は次のとおりです。
649 +
650 + | Command | 確認すること |
651 + | --- | --- |
652 + | `check` | Config や Theme の解決が正しいか |
653 + | `doctor` | Site 全体に問題がないか |
654 + | `inspect config` | 解決済み Theme 設定 |
655 + | `build` | 実際に Site を生成できるか |
656 +
657 + `check` / `doctor` / `inspect` は読み取り専用です。
658 +
659 + これらの Command が Theme File を自動で修正することはありません。
660 +
661 + # 10. 配布用 Package にする
662 +
663 + Site 内 Theme として問題なく動作したら、必要に応じて Package として配布できます。
664 +
665 + Riebeckite Repository 内では、
666 +
667 + ```text id="c7gzq8"
668 + packages/themes/minimal/
669 + ```
670 +
671 + が構成例になります。
672 +
673 + ```text id="5j0p0f"
674 + packages/themes/minimal/
675 + ├─ src/
676 + │ └─ index.ts
677 + ├─ styles/
678 + │ └─ theme.css
679 + ├─ package.json
680 + ├─ README_ja.md
681 + └─ README.md
682 + ```
683 +
684 + `src/index.ts` では Theme Factory を公開します。
685 +
686 + ```ts id="a6e4vn"
687 + import { defineTheme } from "@riebeckite/core";
688 +
689 + export function myTheme() {
690 + return defineTheme({
691 + name: "my-theme",
692 + styles: [
693 + {
694 + moduleSpecifier: "@example/riebeckite-theme/style.css",
695 + },
696 + ],
697 + });
698 + }
699 + ```
700 +
701 + Package では Stylesheet を、
702 +
703 + ```text id="hhg9pd"
704 + ./style.css
705 + ```
706 +
707 + のような Public Export として公開します。
708 +
709 + # 外部 Theme の依存関係
710 +
711 + 配布する Theme は、
712 +
713 + ```text id="n1cn77"
714 + @riebeckite/core
715 + ```
716 +
717 + の Public API に依存します。
718 +
719 + Riebeckite Monorepo 内部の、
720 +
721 + ```text id="ohs3aa"
722 + packages/...
723 + src/...
724 + ../../...
725 + ```
726 +
727 + のような Path に依存させないでください。
728 +
729 + ```mermaid id="52m7im"
730 + flowchart LR
731 + Theme["External Theme"]
732 + Core["@riebeckite/core<br/>Public API"]
733 + Internal["Riebeckite内部Path"]
734 +
735 + Theme --> Core
736 + Theme -.->|"依存しない"| Internal
737 + ```
738 +
739 + 外部利用者が npm から Theme をインストールした場合でも動作する構成にします。
740 +
741 + # Theme 独自 Option
742 +
743 + Theme 固有の設定が必要な場合は、その Theme の Factory Option として定義します。
744 +
745 + たとえば、
746 +
747 + ```ts id="w8b3zk"
748 + myTheme({
749 + // Theme固有Option
750 + });
751 + ```
752 +
753 + のような形です。
754 +
755 + Theme 固有の都合だけで Core の共通 Config を増やすのは避けます。
756 +
757 + ```text id="bwrfbx"
758 + Riebeckite全体で共通
759 + → Core Contract
760 +
761 + そのThemeだけで必要
762 + → Theme Factory Option
763 + ```
764 +
765 + という境界で考えます。
766 +
767 + # Theme がしてはいけないこと
768 +
769 + Theme の責任は Presentation です。
770 +
771 + そのため、
772 +
773 + ```text id="1txwn9"
774 + Routeを追加する
775 + Pageを追加する
776 + Pluginを追加・削除する
777 + JavaScriptの機能を追加する
778 + DOMを変換する
779 + Islandを追加する
780 + ContentManagerを操作する
781 + Filesystemを読む
782 + ```
783 +
784 + といった処理は Theme に入れません。
785 +
786 + 必要な責務に応じて、
787 +
788 + ```mermaid id="sgcd92"
789 + flowchart TD
790 + Need{"何を変更する?"}
791 +
792 + Need -->|"見た目"| Theme["Theme"]
793 + Need -->|"再利用できる機能"| Plugin["Plugin"]
794 + Need -->|"Site固有のPage / 構成"| App["Application"]
795 + Need -->|"Framework共通Contract"| Core["Core"]
796 + ```
797 +
798 + と分けます。
799 +
800 + # 最小の完成形
801 +
802 + 最小の Site 内 Theme は2ファイルで作れます。
803 +
804 + ```text id="wub30o"
805 + extensions/
806 + ├─ local-theme.ts
807 + └─ theme.css
808 + ```
809 +
810 + `local-theme.ts` の中身は次のとおりです。
811 +
812 + ```ts id="f2e0yg"
813 + import { defineTheme } from "@riebeckite/core";
814 +
815 + export function localTheme() {
816 + return defineTheme({
817 + name: "local",
818 + styles: [
819 + {
820 + moduleSpecifier: "/extensions/theme.css",
821 + },
822 + ],
823 + });
824 + }
825 + ```
826 +
827 + `theme.css` の中身は次のとおりです。
828 +
829 + ```css id="0nvzz5"
830 + :is(:root, .rb-theme-root)[data-theme-name="local"] .rb-site {
831 + background: var(--rb-color-paper);
832 + color: var(--rb-color-ink);
833 + }
834 +
835 + :is(:root, .rb-theme-root)[data-theme-name="local"] .rb-article {
836 + max-width: var(--rb-layout-article-max);
837 + }
838 + ```
839 +
840 + そして `riebeckite.config.ts` から、
841 +
842 + ```ts id="hcg2hm"
843 + import { localTheme } from "./extensions/local-theme";
844 +
845 + export default defineConfig({
846 + theme: localTheme(),
847 + });
848 + ```
849 +
850 + と指定します。
851 +
852 + これが Riebeckite Theme の最小構成です。
853 +
854 + # まとめ
855 +
856 + Theme を作るときは、最初から Package 化する必要はありません。
857 +
858 + ```text id="ypbhk"
859 + 少しだけ変更
860 + → userCss
861 +
862 + 独自Designを作る
863 + → Site内Theme
864 +
865 + 再利用・配布したい
866 + → Theme Package
867 + ```
868 +
869 + Theme の CSS では、
870 +
871 + ```text id="doxcc6"
872 + --rb-*
873 + → Semantic Token
874 +
875 + rb-*
876 + → Framework Stable Hook
877 +
878 + rr-<feature>
879 + → Plugin Stable Hook
880 +
881 + Theme Root Selector
882 + → ThemeのStyleを適用する範囲
883 + ```
884 +
885 + を利用します。
886 +
887 + そして、**Theme が Page Type や内部 Component の構造を知りすぎない**ことが最も重要です。
888 +
889 + ```text id="3p18me"
890 + Page Type
891 + ↓
892 + Stable Hook / Semantic Token
893 + ↓
894 + Theme
895 + ```
896 +
897 + という境界を保つことで、新しい Plugin や Page Type が追加されても利用できる Theme を作れます。
898 +
899 + ## 関連資料
900 +
901 + - [Theme](./README.ja.md) — Theme の選択と利用方法
902 + - [テーマ作成の詳細](../framework/theme-system.ja.md) — Option、Token、Hook、Cascade、配布を含む詳しい設計
903 + - [Theme API](../reference/theme-api.ja.md) — `defineTheme` と Theme の公開 Contract
904 + - [はじめてのプラグイン作成](../plugins/writing-a-plugin.ja.md) — 機能を追加する場合
905 + - [Plugin API](../reference/plugin-api.ja.md) — Plugin の公開 Contract
906 + - [アクセシビリティ](../accessibility.ja.md) — Theme 作成時のアクセシビリティ
907 +