---
title: "カスタムテーマ — zenit Zig UI ドキュメント"
description: "テーマは 1 つの ThemeTokens 値です。色、余白、角丸、文字サイズ、コントロール寸法がすべて入っています。"
url: https://zenit.z.express/ja/docs/guide/theming
language: ja
alternate_en: https://zenit.z.express/docs/guide/theming.md
alternate_zh: https://zenit.z.express/zh/docs/guide/theming.md
alternate_es: https://zenit.z.express/es/docs/guide/theming.md
alternate_ko: https://zenit.z.express/ko/docs/guide/theming.md
alternate_fr: https://zenit.z.express/fr/docs/guide/theming.md
alternate_de: https://zenit.z.express/de/docs/guide/theming.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# カスタムテーマ

テーマは 1 つの ThemeTokens 値です。色、余白、角丸、文字サイズ、コントロール寸法がすべて入っています。組み込みプリセットをコピーし、ブランドに関わるフィールドを変更して cx.setTheme で設定すれば、それがあなたのテーマです。

このページではテーマの定義と設定方法を説明します。ビューが token をどう使うか（スタイル関数や recipe）は[スタイルとテーマ](https://zenit.z.express/ja/docs/guide/styling)で扱います。

## デフォルトテーマ

各ウィンドウの `Cx` は最初 `ui.theme.dark` を指しています。`setTheme` を一度も呼ばないアプリはダークになります（Hello Button がそうです）。別のテーマを使うには、ノードが作られる前、マウント関数の 1 行目で設定します。

`main.zig`

```zig
fn mountUI(cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node {
    // Cx starts on ui.theme.dark. Pick the theme before building any node,
    // so every component reads the right tokens when it mounts.
    cx.setTheme(&ui.theme.light);

    const root = try ui.boxStyled(cx, S.root, .{});
    // …
    return root;
}
```

複数ウィンドウでは各ウィンドウが独自の `Cx` を持つため、それぞれのマウント関数で設定します。zenit は現時点でシステムの外観に自動で追従**しません**。システムの明暗切り替えは再描画を 1 回起こすだけで、システムの外観を読む公開 API もありません。必要ならユーザーに切り替えスイッチを用意してください（後述の「実行時の切り替え」を参照）。

## ThemeTokens の構造

`src/ui/theme.zig（抜粋）`

```zig
pub const ThemeTokens = struct {
    name: []const u8 = "unnamed",
    scheme: ColorScheme = .light,      // .light / .dark — the one flag for "is this dark?"
    color: ColorTokens,                // 54 colors, no defaults: you must provide all of them
    space: SpaceScale = .{},           // _0 … _12  (0 … 48 px, 4 px grid)
    radius: RadiusScale = .{},         // none sm md lg xl full
    font_size: FontSizeScale = .{},    // xxs … xxxl (9 … 24)
    border_width: BorderWidthScale = .{},
    size: SizeTokens = .{},            // checkbox, switch, badge, dot, close button
    control: ControlScale = .{},       // xs / sm / md / lg metrics for every control
    duration: DurationTokens = .{},    // fast / normal / slow (seconds)
    shadow: ShadowTokens = .{},        // sm / md / lg
};
```

`color` 以外のフィールドにはすべて既定値があります。`ColorTokens` の 54 色には既定値がないため、struct リテラルを一から書かないでください。`ui.theme.light`、`ui.theme.dark`、`ui.theme.high_contrast` のいずれかをコピーし、必要なフィールドだけを変更します。

> NOTE
> 
> **明暗の判定には scheme だけを使います。** `scheme` は唯一の明暗フラグです。Notifier はこれでパレットを選び、DevTools はこれから自身の配色を導きます。`name` に “dark” が含まれるかで判断しないでください。ハイコントラストテーマは真っ黒の背景ですが、名前に dark は含まれていません。

## ブランドテーマの派生

テーマはコンテナレベルの `const` として書きます。初期化ブロックはコンパイル時に評価され、結果は静的ストレージに置かれるため、`cx.setTheme(&brand)` が保持するポインタはプログラムの実行中ずっと有効です。関数のローカル変数に置いたテーマのアドレスを取らないでください。

`theme.zig`

```zig
// theme.zig — your app's themes. Container-level consts: they are
// evaluated at compile time and live for the whole program, which is
// what cx.setTheme(&brand) needs (it keeps the pointer).
const ui = @import("ui");

pub const brand: ui.ThemeTokens = blk: {
    var t = ui.theme.light; // copy a complete preset (every color is required)
    t.name = "brand";
    t.scheme = .light;

    const blue = ui.Color.hex(0x1144AA);
    t.color.accent = blue;                       // links, checked states, active tabs…
    t.color.accent_hover = ui.Color.hex(0x0D3688);
    t.color.accent_subtle = ui.Color.hex(0xE6ECF7);
    t.color.button_primary_bg = blue;            // primary buttons read these two,
    t.color.button_primary_fg = ui.Color.WHITE;  //   not accent
    t.color.border_focus = blue;
    t.color.selection_bg = ui.Color.hex(0xC9D6F0);
    t.color.list_selection_bg = ui.Color.hex(0xE6ECF7);

    t.radius.lg = 10;
    t.control.md.radius = 10;
    t.control.md.padding_h = 14;
    break :blk t;
};
```

> WARNING
> 
> **コンポーネントの token は accent に追従しません。** プリセットでは `accent` と `button_primary_bg` は独立した値で、派生関係はありません。ブランドカラーを変えるときは `accent`、`button_primary_bg` / `button_primary_fg`、`border_focus` を一緒に変更してください。そうしないと primary ボタンはプリセットの色のままです。

## 対になるダークテーマ

ダーク版はライトのブランドテーマの背景を塗り替えるのではなく、`ui.theme.dark` から派生させます。ダークプリセットの数十個の背景・文字・枠線の値は一式で調整されているからです。暗い背景ではブランドカラーを明るくする必要があることが多く、primary ボタンの文字は暗い色にします。

`theme.zig`

```zig
pub const brand_dark: ui.ThemeTokens = blk: {
    var t = ui.theme.dark; // start from the dark preset, not from brand
    t.name = "brand_dark";
    t.scheme = .dark;      // Notification palettes and DevTools key off this

    const blue = ui.Color.hex(0x7FA2F0); // lighter on dark backgrounds
    t.color.accent = blue;
    t.color.button_primary_bg = blue;
    t.color.button_primary_fg = ui.Color.hex(0x0D1119);
    t.color.border_focus = blue;
    break :blk t;
};
```

![brand（ライト）](https://zenit.z.express/media/theme-brand.webp?v=588f003545)brand（ライト）

![brand_dark](https://zenit.z.express/media/theme-brand-dark.webp?v=854df7b29f)brand\_dark

![ui.theme.dark（デフォルト）](https://zenit.z.express/media/theme-default-dark.webp?v=3b5b953dc1)ui.theme.dark（デフォルト）

同じ Hello Button で setTheme の引数だけを変えたもの。3 枚とも E2E harness が実際のウィンドウで撮影しました。

## どの token が何に効くか

コンポーネントが実際に読むフィールドで整理しています（v0.1.0-alpha のソース）。テーマを作るときはまずこれらを変更します。

| Token | 主な使用先 |
| --- | --- |
| `fg_primary · fg_secondary · fg_tertiary · fg_disabled` | ほぼすべての文字とアイコン |
| `bg_primary · bg_secondary · bg_tertiary` | パネル、カード、Calendar、Chip、Progress / Slider のトラック、Skeleton |
| `bg_hover · bg_active` | クリック可能なコントロールのホバーと押下状態 |
| `accent` | 選択された Checkbox / Switch、リンク、Tabs、Breadcrumb、選択日、Badge |
| `button_primary_bg · button_primary_fg` | primary Button（hover / 押下は背景から派生）。fg は Badge、Chip、Tabs の反転文字にも使用 |
| `border · border_strong · separator` | 枠線と区切り線 |
| `border_focus · input_bg · input_border · selection_bg` | Input、Select、NumberStepper、DatePicker などの入力系コントロール |
| `success · warning · danger · info · status_fg` | Alert、Badge、Tag、Progress、danger Button |
| `list_hover_bg · list_selection_bg` | Menu、DropdownMenu、ComboBox、Table、Tree、DataTable |
| `checkbox_* · switch_thumb · switch_track_off` | Checkbox と Switch の未選択・無効状態 |
| `tooltip_bg · tooltip_fg · overlay · scrollbar_thumb` | Tooltip、Modal / Sheet の背景幕、スクロールバー |

> WARNING
> 
> **これらの token を読むコンポーネントはまだありません。** `accent_muted`、`bg_inset`、`button_secondary_bg`、`button_secondary_fg`、`switch_track_on`（Switch のオン状態は `accent` を使用）、`table_header_bg`、`list_selection_hover_bg`、そして `success_subtle` / `warning_subtle` / `danger_subtle` / `info_subtle`。現在これらを変更しても見た目は変わりません。自分のスタイル関数では通常どおり使えます。

一部のコンポーネントは `ColorTokens` を読まず独自の配色を持っています。Notifier は `scheme` に応じて組み込みのライト / ダークパレットを選び、GlassBox のガラスの色味や Progress と Select の一部の細部も固定値です。明暗には追従しますが、ブランドカラーには変わりません。

## 角丸、余白、コントロール寸法

色以外の token はフィールド単位で上書きできます。最も役立つのは `control` です。Button、Input、Select、ComboBox、DatePicker、DateRangePicker、Tabs、Chip は xs / sm / md / lg の 4 段階の寸法を共有しています。コントロールの高さは token ではなく、計算で求められます。

| サイズ | 既定の高さ = padding\_y × 2 + font\_size × line\_height |
| --- | --- |
| `xs` | 2.5 × 2 + 12 × 1.25 = 20 |
| `sm` | 4.5 × 2 + 12 × 1.25 = 24 |
| `md` | 7.25 × 2 + 14 × 1.25 = 32 |
| `lg` | 10 × 2 + 16 × 1.25 = 40 |

つまり md コントロールを高くしたいなら、`t.control.md.padding_y` か文字サイズを変更すれば、同じサイズのすべてのコントロールが一緒に変わります。コントロールに `height` を直書きしないでください。アイコンサイズ `icon_size` は行の高さを超えないようにします。

## 実行時の切り替え

`cx.setTheme` はいつでも呼び出せます。token ポインタを差し替え、`cx.root` サブツリー（オーバーレイ portal を含む）で `on_theme` / `before_render` hook を再実行し、再描画を要求します。

`prefs.zig`

```zig
const Prefs = struct {
    cx: *ui.Cx,
    dark: bool = false,

    pub fn toggle(self: *Prefs) void {
        self.dark = !self.dark;
        self.cx.setTheme(if (self.dark) &themes.brand_dark else &themes.brand);
    }
};

// In mountUI:
const prefs = try cx.bindState(Prefs, .{ .cx = cx });
cx.setTheme(&themes.brand);
// …
.on_click = cx.on(Prefs, prefs, Prefs.toggle),
```

```zig
// A reactive read of the current theme (lives as long as `scope`).
const theme_sig = try cx.themeSignal(scope);
const is_dark = theme_sig.get().scheme == .dark;
```

> NOTE
> 
> **実行時の切り替えが及ぶ範囲。** `boxStyled` / `textStyled` で作ったノードと、テーマ hook を登録したコンポーネントはすぐに色が変わります。多くの `ui.widgets.*` はマウント時に token を読むため、完全に切り替えるには該当するサブツリーを再構築する必要があります。起動時に一度だけテーマを選ぶアプリには影響ありません。

## リリース前のチェック

-   `scheme` が背景の明暗と一致している（暗い背景なら必ず `.dark`）。
    
-   `button_primary_fg` と `button_primary_bg`、`status_fg` と `danger` のコントラスト比が 4.5:1 以上ある（組み込みプリセットのユニットテストと同じ基準）。
    
-   `border` が `bg_primary` と見分けられる（プリセットのテストはコントラスト比 ≥ 1.2 を要求）。
    
-   Storybook を自分のテーマに切り替え、よく使うコンポーネントを一通り操作する（Component Wall story が手始めに便利）。
