---
title: "スタイルとテーマ — zenit Zig UI ドキュメント"
description: "Token、名前付きスタイル関数、Recipe を使い、見た目とビジネスロジックを互いに独立させます。"
url: https://zenit.z.express/ja/docs/guide/styling
language: ja
alternate_en: https://zenit.z.express/docs/guide/styling.md
alternate_zh: https://zenit.z.express/zh/docs/guide/styling.md
alternate_es: https://zenit.z.express/es/docs/guide/styling.md
alternate_ko: https://zenit.z.express/ko/docs/guide/styling.md
alternate_fr: https://zenit.z.express/fr/docs/guide/styling.md
alternate_de: https://zenit.z.express/de/docs/guide/styling.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# スタイルとテーマ

Token、名前付きスタイル関数、Recipe を使い、見た目とビジネスロジックを互いに独立させます。

## 3 層スタイルモデル

**01 · ThemeTokens**

色、文字サイズ、余白、角丸、コントロール寸法の唯一の情報源。

**02 · styles.zig**

名前を付け、再利用し、テストでき、テーマ変更時に再実行される純粋関数。

**03 · Recipe**

バリアント、インタラクションの条件状態、複数パーツのコンポーネントを扱います。

STYLE LAYERS

各層はすぐ下の層だけを読みます。ビューは色を直書きしないので、テーマの切り替えは最下層を差し替えるだけで済みます。

[Video](https://zenit.z.express/media/stories/button.mp4?v=6203a01415)

Button の variant × size、アイコン、loading、disabled、block のマトリクス。スタイルは recipe で解決されます。

[Video](https://zenit.z.express/media/stories/glassbox.mp4?v=570227ca3d)

GlassBox の regular・interactive・clear モードと本物の backdrop blur。

## Token ファースト

スタイル値は `cx.tokens`（またはスタイル関数が受け取る `t`）から読み取ります。本当にスケール外のデザイン値は `ui.arb` で明示します。実行時にはコストゼロの恒等関数で、価値は意味づけにあります。読み手は「意図的な任意値」と「token に移すべき手抜きのリテラル」をひと目で区別できます。

```zig
// Preferred: follows the theme
.background = cx.tokens.color.bg_primary,
.padding = ui.Padding.all(cx.tokens.space._4),
.font_size = cx.tokens.font_size.xl,

// Legitimate escape hatch: states that the value is intentionally off-scale
.font_size = ui.arb.px(18),
.background = ui.arb.hex(0x316ff6),
.border_color = ui.arb.hexA(0x000000, 0.12),
```

> WARNING
> 
> **裸のリテラルは避けてください。** ビュー内の `Color.hex(...)` や固定の `font_size` はテーマを迂回します。`scripts/check_style_literals.sh` は裸のリテラルだけを検出し `ui.arb` は通すので、`ui.arb` を grep すれば token 外の値をすべて棚卸しできます。同じ arb 値が 3 回以上現れたら、token への昇格を検討してください。

## 名前付きスタイル関数

スタイル関数のシグネチャは `fn (*const ui.ThemeTokens) ui.BoxStyle`（または `ui.TextStyle`）です。`styles.zig` にまとめ、ビューからは名前で参照します。

`styles.zig`

```zig
const ui = @import("ui");

pub fn card(t: *const ui.ThemeTokens) ui.BoxStyle {
    return .{
        .background = t.color.bg_secondary,
        .padding = ui.Padding.all(t.space._6),
        .gap = t.space._3,
    };
}

pub fn row(t: *const ui.ThemeTokens) ui.BoxStyle {
    return .{ .gap = t.space._2 };
}

pub fn title(t: *const ui.ThemeTokens) ui.TextStyle {
    return .{
        .font_size = t.font_size.xxl,
        .font_weight = 600,
        .color = t.color.fg_primary,
    };
}
```

`view.zig`

```zig
const S = @import("styles.zig");

const card = try ui.boxStyled(cx, S.card, .{});
try card.appendChild(
    cx.allocator,
    try ui.textStyled(cx, S.title, "Workspace"),
);

// hstackStyled / vstackStyled force direction = .row / .column.
const actions = try ui.hstackStyled(cx, S.row, .{});
try card.appendChild(cx.allocator, actions);
```

`boxStyled`、`textStyled`、`hstackStyled`、`vstackStyled` はノードに `on_theme` hook を登録し、スタイル関数そのものを記憶します。テーマが変わると、ノードは新しい token でスタイルを再計算します。通常の `ui.box` 内で読んだ token は、マウント時のスナップショットにすぎません。

## テーマの切り替え

組み込みテーマは `ui.theme.light`、`ui.theme.dark`、`ui.theme.high_contrast` の 3 つです。`cx.setTheme` は token ポインタを差し替え、`cx.root` サブツリーを走査して各ノードの `on_theme` と `before_render` hook を再実行し、dirty にします。現在のテーマをリアクティブに読むには `cx.themeSignal(scope)` を使います。

`theme.zig`

```zig
// Built-in palettes: ui.theme.light, ui.theme.dark, ui.theme.high_contrast
cx.setTheme(&ui.theme.dark);

// Reactive access: a Signal that setTheme updates.
const theme = try cx.themeSignal(scope);
try scope.createEffect(.{ .theme = theme }, struct {
    fn run(ctx: anytype) void {
        const t = ctx.theme.get();
        std.log.info("theme: {s} ({s})", .{ t.name, @tagName(t.scheme) });
    }
}.run);
```

> NOTE
> 
> **再実行の範囲は cx.root サブツリーです。** コンポーネントのオーバーレイ（Modal、Popover、Menu、Tooltip）は `cx.root` 配下の `WindowOverlayPortal` にあるため、`setTheme` で一緒に再実行されます。範囲外になるのは `cx.root` から切り離されたノードツリーだけです。再実行されるのは styled ノードが登録した hook です。多くの `ui.widgets.*` はマウント時に token をスナップショットするため、依然として再構築が必要です。

明暗の違いは完全な token パレットで表現すべきです。スタイル関数内で本当に分岐が必要なときだけ `t.scheme`（`.light` / `.dark`）を読みます。

ブランドカラーの適用や独自のライト / ダークテーマの作成については、[カスタムテーマ](https://zenit.z.express/ja/docs/guide/theming)を参照してください。

## Recipe と条件状態

`ui.recipe` は名前空間です。`ui.recipe.recipe(Config)` は単一ノードの recipe を、`ui.recipe.slotRecipe(Config)` は複数パーツの recipe を定義します。解決時は **base → variants → derived → compounds** の順に合成され、後から書かれた非 null フィールドが優先されます。コンポーネント外部からの style override は最後に適用されます。

RECIPE MERGE

各ステップは自分が設定したフィールドだけを上書きします。慣例として derived はジオメトリ（padding、角丸、サイズ）だけを出力し、background には触れません。

| Config メンバー | 役割 |
| --- | --- |
| `Variants` | 必須。各フィールドがバリアントの次元（enum または bool）で、既定値を持つ |
| `base(t)` | 任意。ベースとなる ConditionalStyle |
| `variants` | 任意。次元ごとに 1 つの resolver：fn (value, t) ConditionalStyle |
| `derived(v, t)` | 任意。Variants 全体を受け取り、次元をまたぐ連続的なロジックを表す（例：padding = f(size, アイコンモード)） |
| `compounds` | 任意。{ matches(v), style(t) }。複数の次元が同時に一致したときに重ねて適用 |

以下はリポジトリ内 `examples/hello_button/styles.zig` の実際のコードです。アプリ側でも独自の recipe を定義し、`resolveBase` でバリアントを固定して `ui.boxStyled` に渡せるスタイル関数にできます。

`examples/hello_button/styles.zig`

```zig
pub const PanelEmphasis = enum { neutral, highlight };

/// CVA-style: base + one variant dimension, all from tokens.
const PanelRecipe = ui.recipe.recipe(struct {
    pub const Variants = struct {
        emphasis: PanelEmphasis = .neutral,
    };

    pub fn base(t: *const ui.ThemeTokens) ui.ConditionalStyle {
        return .{ .base = .{
            .padding = ui.Padding.symmetric(t.space._2, t.space._4),
            .corner_radius = t.radius.md,
        } };
    }

    pub const variants = .{
        .emphasis = struct {
            fn resolve(e: PanelEmphasis, t: *const ui.ThemeTokens) ui.ConditionalStyle {
                return switch (e) {
                    .neutral => .{ .base = .{ .background = t.color.bg_secondary } },
                    .highlight => .{ .base = .{
                        .background = t.color.bg_tertiary,
                        .border = .{ .width = 1, .color = t.color.accent, .radius = 0 },
                    } },
                };
            }
        }.resolve,
    };
});

/// Freeze a variant into a theme-safe style function for ui.boxStyled.
pub fn panel(comptime emphasis: PanelEmphasis) fn (*const ui.ThemeTokens) ui.BoxStyle {
    return struct {
        fn f(t: *const ui.ThemeTokens) ui.BoxStyle {
            return PanelRecipe.resolveBase(.{ .emphasis = emphasis }, t);
        }
    }.f;
}
```

`ConditionalStyle` は `base` のほかに 7 つの条件スロットを持ちます：`selected`、`expanded`、`hover`、`active`、`focus`、`invalid`、`disabled`。`resolve(state)` は selected → expanded → hover → active → focus → invalid の順に重ねます。`disabled` はショートサーキットし、無効化されたノードには他の条件は一切重なりません。

`resolve.zig`

```zig
// Full ConditionalStyle for one variant combination.
const cs = PanelRecipe.resolve(.{ .emphasis = .highlight }, cx.tokens);

// A component's style props are applied last, on top of the recipe output.
const merged = cs.override(.{
    .style = .{ .background = cx.tokens.color.accent },
});

// Pick the final style for the current interaction state.
const final: ui.BoxStyle = merged.resolve(.{ .is_hovered = true });
```

| 場面 | ツール |
| --- | --- |
| 単一の静的スタイル | 名前付きスタイル関数 |
| サイズ / 見た目のバリアント | `ui.recipe.recipe` |
| 複数パーツのコンポーネント（入力欄、メニュー） | `ui.recipe.slotRecipe` |
| hover / disabled / invalid | `ui.ConditionalStyle` |
| ライト / ダーク | 完全な token パレット。必要なら `t.scheme` を読む |

## 現在の制約

-   **コンポーネントの多くはマウント時に token を読みます。**`ui.widgets.*` のうち token を再読み込みする hook を登録していない部分は、マウント時のスナップショットを保持します。実行時にテーマを切り替えると、アプリ側の styled ノードは再実行されますが、コンポーネントライブラリのノードはツリーの再構築が必要になる場合があります。
    
-   **再実行は cx.root サブツリーのみが対象です。**ウィンドウのオーバーレイ portal はその中にあります。`cx.root` から切り離されたツリーは更新されません。
    
-   **themeSignal はそれを作成した Scope に従います。**すべての購読者より長く生きるよう、アプリのルート Scope で作成してください。
    

各コンポーネントの variant とサイズのマトリクスは[コンポーネントサイト](https://zenit.z.express/ja/components)を参照してください。
