docs/guide/theming
Kernkonzepte · Themes

Eigene Themes

Ein Theme ist ein einziger ThemeTokens-Wert: Farben, Abstände, Radien, Schriftgrößen und Control-Maße. Kopieren Sie ein eingebautes Preset, ändern Sie die Felder, die Ihre Marke tragen, installieren Sie es mit cx.setTheme. Fertig ist Ihr Theme.

8 Min. Lesezeit · Screenshots aus einem echten Fenster

Diese Seite behandelt das Definieren und Installieren eines Themes. Wie Views Tokens nutzen (Stilfunktionen und Recipes), steht unter Styling und Themes.

Das Standard-Theme

Das Cx jedes Fensters startet mit ui.theme.dark. Eine App, die nie setTheme aufruft, ist dunkel, so wie Hello Button. Für ein anderes Theme setzen Sie es in der ersten Zeile Ihrer Mount-Funktion, bevor irgendein Node existiert:

main.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;
}

Bei mehreren Fenstern hat jedes sein eigenes Cx, setzen Sie das Theme also in jeder Mount-Funktion. zenit folgt dem Erscheinungsbild des Systems noch nicht: Ein Hell/Dunkel-Wechsel des Systems löst nur ein Neuzeichnen aus, und es gibt keine öffentliche API, um das System-Erscheinungsbild zu lesen. Bieten Sie Ihren Nutzern stattdessen einen Umschalter an (siehe „Wechsel zur Laufzeit“).

Aufbau von ThemeTokens

src/ui/theme.zig (Auszug)
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
};

Alles außer color hat Standardwerte. Die 54 Farben in ColorTokens haben keine, schreiben Sie also kein Struct-Literal von Grund auf. Kopieren Sie ui.theme.light, ui.theme.dark oder ui.theme.high_contrast und ändern Sie nur, was Sie brauchen.

Ein Marken-Theme ableiten

Schreiben Sie Themes als consts auf Container-Ebene; der Initialisierungsblock läuft zur Compile-Zeit und das Ergebnis liegt im statischen Speicher, sodass der Zeiger, den cx.setTheme(&brand) behält, während der gesamten Programmlaufzeit gültig bleibt. Nehmen Sie nie die Adresse eines Themes, das in einer lokalen Variable einer Funktion liegt.

theme.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;
};

Ein passendes dunkles Theme

Leiten Sie die dunkle Variante von ui.theme.dark ab, statt das helle Marken-Theme umzufärben: Die Dutzenden Hintergrund-, Text- und Rahmenwerte im dunklen Preset sind als Satz abgestimmt. Auf dunklen Hintergründen muss die Markenfarbe meist aufgehellt werden, und der Text von Primary-Buttons wird dunkel.

theme.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 (hell)brand (hell)
brand_darkbrand_dark
ui.theme.dark (Standard)ui.theme.dark (Standard)
Derselbe Hello Button, nur das Argument von setTheme ist geändert. Alle drei wurden vom E2E-harness im echten Fenster aufgenommen.

Welche Tokens was steuern

Gruppiert danach, was Komponenten tatsächlich lesen (Quellcode v0.1.0-alpha). Beginnen Sie beim Theming mit diesen:

TokenHauptsächlich genutzt von
fg_primary · fg_secondary · fg_tertiary · fg_disabledNahezu alle Texte und Icons
bg_primary · bg_secondary · bg_tertiaryPanels, Karten, Calendar, Chip, Progress- / Slider-Spuren, Skeleton
bg_hover · bg_activeHover- und Gedrückt-Zustände klickbarer Controls
accentAktivierte Checkbox / Switch, Links, Tabs, Breadcrumb, ausgewählte Daten, Badge
button_primary_bg · button_primary_fgPrimary Button (hover / gedrückt aus dem Hintergrund abgeleitet); fg auch für invertierten Text auf Badge, Chip und Tabs
border · border_strong · separatorRahmen und Trennlinien
border_focus · input_bg · input_border · selection_bgInput, Select, NumberStepper, DatePicker und andere Felder
success · warning · danger · info · status_fgAlert, Badge, Tag, Progress, danger Button
list_hover_bg · list_selection_bgMenu, DropdownMenu, ComboBox, Table, Tree, DataTable
checkbox_* · switch_thumb · switch_track_offNicht aktivierte und deaktivierte Checkbox / Switch
tooltip_bg · tooltip_fg · overlay · scrollbar_thumbTooltip, Modal- / Sheet-Abdunklung, Scrollbars

Einige Komponenten bringen eigene Farben mit, statt ColorTokens zu lesen: Der Notifier wählt anhand von scheme zwischen seinen eingebauten hellen und dunklen Paletten, und GlassBox-Tönungen sowie einige Details von Progress und Select sind feste Werte. Sie folgen Hell/Dunkel, aber nicht Ihrer Markenfarbe.

Radien, Abstände und Control-Maße

Nicht-Farb-Tokens lassen sich Feld für Feld überschreiben. Am nützlichsten ist control: Button, Input, Select, ComboBox, DatePicker, DateRangePicker, Tabs und Chip teilen sich vier Maßsätze, xs / sm / md / lg. Die Höhe eines Controls ist kein Token. Sie wird berechnet:

GrößeStandardhöhe = padding_y × 2 + font_size × line_height
xs2.5 × 2 + 12 × 1.25 = 20
sm4.5 × 2 + 12 × 1.25 = 24
md7.25 × 2 + 14 × 1.25 = 32
lg10 × 2 + 16 × 1.25 = 40

Um md-Controls höher zu machen, ändern Sie also t.control.md.padding_y oder die Schriftgröße, und alle md-Controls folgen; codieren Sie keine height für ein Control fest. Halten Sie icon_size nicht größer als die Zeilenhöhe.

Wechsel zur Laufzeit

cx.setTheme kann jederzeit aufgerufen werden: Es tauscht den Token-Zeiger aus, spielt die on_theme- / before_render-Hooks im gesamten cx.root-Teilbaum (inklusive Overlay-Portal) erneut ab und fordert ein Neuzeichnen an.

prefs.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),
reactive read
// 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;

Vor der Auslieferung

  • ✓

    scheme passt zum Hintergrund (ein dunkler Hintergrund muss .dark sein).

  • ✓

    button_primary_fg auf button_primary_bg und status_fg auf danger erreichen mindestens 4.5:1, dieselbe Prüfung, mit der die eingebauten Presets getestet werden.

  • ✓

    border bleibt von bg_primary unterscheidbar (die Preset-Tests verlangen ≥ 1.2).

  • ✓

    Stellen Sie das Storybook auf Ihr Theme um und klicken Sie sich durch die gängigen Komponenten (die Story Component Wall ist ein guter Anfang).

zenit · DoppellizenzKostenlos für Open-Source-Projekte unter GPL-3.0-only; Closed-Source- oder kommerzielle Produkte benötigen eine kommerzielle Lizenz.Kontakt zum Autor: zongyi.xzy#gmail.com (# durch @ ersetzen)zenit 4c50245 2026-10-01