カスタムテーマ
テーマは 1 つの ThemeTokens 値です。色、余白、角丸、文字サイズ、コントロール寸法がすべて入っています。組み込みプリセットをコピーし、ブランドに関わるフィールドを変更して cx.setTheme で設定すれば、それがあなたのテーマです。
このページではテーマの定義と設定方法を説明します。ビューが token をどう使うか(スタイル関数や recipe)はスタイルとテーマで扱います。
デフォルトテーマ
各ウィンドウの Cx は最初 ui.theme.dark を指しています。setTheme を一度も呼ばないアプリはダークになります(Hello Button がそうです)。別のテーマを使うには、ノードが作られる前、マウント関数の 1 行目で設定します。
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 の構造
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 のいずれかをコピーし、必要なフィールドだけを変更します。
ブランドテーマの派生
テーマはコンテナレベルの const として書きます。初期化ブロックはコンパイル時に評価され、結果は静的ストレージに置かれるため、cx.setTheme(&brand) が保持するポインタはプログラムの実行中ずっと有効です。関数のローカル変数に置いたテーマのアドレスを取らないでください。
// 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;
};対になるダークテーマ
ダーク版はライトのブランドテーマの背景を塗り替えるのではなく、ui.theme.dark から派生させます。ダークプリセットの数十個の背景・文字・枠線の値は一式で調整されているからです。暗い背景ではブランドカラーを明るくする必要があることが多く、primary ボタンの文字は暗い色にします。
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(ライト)
brand_dark
ui.theme.dark(デフォルト)どの token が何に効くか
コンポーネントが実際に読むフィールドで整理しています(v0.1.0-alpha のソース)。テーマを作るときはまずこれらを変更します。
fg_primary · fg_secondary · fg_tertiary · fg_disabledほぼすべての文字とアイコンbg_primary · bg_secondary · bg_tertiaryパネル、カード、Calendar、Chip、Progress / Slider のトラック、Skeletonbg_hover · bg_activeクリック可能なコントロールのホバーと押下状態accent選択された Checkbox / Switch、リンク、Tabs、Breadcrumb、選択日、Badgebutton_primary_bg · button_primary_fgprimary Button(hover / 押下は背景から派生)。fg は Badge、Chip、Tabs の反転文字にも使用border · border_strong · separator枠線と区切り線border_focus · input_bg · input_border · selection_bgInput、Select、NumberStepper、DatePicker などの入力系コントロールsuccess · warning · danger · info · status_fgAlert、Badge、Tag、Progress、danger Buttonlist_hover_bg · list_selection_bgMenu、DropdownMenu、ComboBox、Table、Tree、DataTablecheckbox_* · switch_thumb · switch_track_offCheckbox と Switch の未選択・無効状態tooltip_bg · tooltip_fg · overlay · scrollbar_thumbTooltip、Modal / Sheet の背景幕、スクロールバー一部のコンポーネントは ColorTokens を読まず独自の配色を持っています。Notifier は scheme に応じて組み込みのライト / ダークパレットを選び、GlassBox のガラスの色味や Progress と Select の一部の細部も固定値です。明暗には追従しますが、ブランドカラーには変わりません。
角丸、余白、コントロール寸法
色以外の token はフィールド単位で上書きできます。最も役立つのは control です。Button、Input、Select、ComboBox、DatePicker、DateRangePicker、Tabs、Chip は xs / sm / md / lg の 4 段階の寸法を共有しています。コントロールの高さは token ではなく、計算で求められます。
xs2.5 × 2 + 12 × 1.25 = 20sm4.5 × 2 + 12 × 1.25 = 24md7.25 × 2 + 14 × 1.25 = 32lg10 × 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 を再実行し、再描画を要求します。
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),// 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;リリース前のチェック
- ✓
schemeが背景の明暗と一致している(暗い背景なら必ず.dark)。 - ✓
button_primary_fgとbutton_primary_bg、status_fgとdangerのコントラスト比が 4.5:1 以上ある(組み込みプリセットのユニットテストと同じ基準)。 - ✓
borderがbg_primaryと見分けられる(プリセットのテストはコントラスト比 ≥ 1.2 を要求)。 - ✓
Storybook を自分のテーマに切り替え、よく使うコンポーネントを一通り操作する(Component Wall story が手始めに便利)。