docs/guide/styling
Kernkonzepte · Design system

Styling und Themes

Mit Tokens, benannten Stilfunktionen und Recipes bleiben Optik und Geschäftslogik voneinander unabhängig.

12 Min. Lesezeit

Das Drei-Schichten-Modell

01
01 · ThemeTokens

Die einzige Quelle für Farben, Schriftgrößen, Abstände, Radien und Control-Maße.

02
02 · styles.zig

Reine Funktionen, die Sie benennen, wiederverwenden, testen — und beim Theme-Wechsel erneut ausführen können.

03
03 · Recipe

Behandelt Varianten, Interaktionszustände und mehrteilige Komponenten.

STYLE LAYERS
Jede Schicht liest nur die darunterliegende: Views codieren nie eine Farbe fest, ein Theme-Wechsel tauscht also nur die unterste Schicht aus.
Die Button-Matrix aus variant × size, Icon, loading, disabled und block, gestylt über eine Recipe.
GlassBox in den Modi regular, interactive und clear mit echtem Backdrop-Blur.

Tokens zuerst

Lesen Sie Stilwerte aus cx.tokens (oder dem t, das eine Stilfunktion erhält). Designwerte, die wirklich außerhalb der Skala liegen, markieren Sie mit ui.arb: Zur Laufzeit ist das eine kostenlose Identitätsfunktion, ihr Wert ist semantisch — Lesende unterscheiden einen bewusst gewählten Sonderwert von einem bequemen Literal, das ein Token werden sollte.

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),

Benannte Stilfunktionen

Eine Stilfunktion hat die Signatur fn (*const ui.ThemeTokens) ui.BoxStyle (oder ui.TextStyle). Sammeln Sie sie in styles.zig und lassen Sie Views sie über ihren Namen referenzieren.

styles.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
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 und vstackStyled registrieren am Node einen on_theme-Hook, der sich die Stilfunktion selbst merkt. Wechselt das Theme, berechnet der Node seinen Stil aus den neuen Tokens neu; in einem einfachen ui.box gelesene Tokens sind nur ein Snapshot vom Zeitpunkt des Mountens.

Themes wechseln

Drei Themes sind eingebaut: ui.theme.light, ui.theme.dark und ui.theme.high_contrast. cx.setTheme tauscht den Token-Zeiger aus, durchläuft den cx.root-Teilbaum, spielt dabei die on_theme- und before_render-Hooks jedes Nodes erneut ab und markiert sie als dirty. Für reaktiven Zugriff auf das aktuelle Theme verwenden Sie cx.themeSignal(scope).

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

Hell/Dunkel-Unterschiede gehören in die vollständige Token-Palette; lesen Sie t.scheme (.light / .dark) in einer Stilfunktion nur, wenn Sie wirklich verzweigen müssen.

Wie Sie Ihre Markenfarbe anwenden oder eigene helle / dunkle Themes bauen, lesen Sie unter Eigene Themes.

Recipes und Bedingungen

ui.recipe ist ein Namespace: ui.recipe.recipe(Config) definiert eine Recipe für einen einzelnen Node, ui.recipe.slotRecipe(Config) eine mehrteilige. Die Auflösung führt base → variants → derived → compounds zusammen, spätere Nicht-null-Felder gewinnen; der externe Style-Override einer Komponente wird zuletzt angewendet.

RECIPE MERGE
Jeder Schritt überschreibt nur die Felder, die er setzt. Per Konvention erzeugt derived nur Geometrie (Padding, Radius, Größe) und rührt background nie an.
Config-MemberRolle
VariantsPflicht. Jedes Feld ist eine Variantendimension (enum oder bool) mit Standardwert
base(t)Optional. Der Basis-ConditionalStyle
variantsOptional. Ein Resolver pro Dimension: fn (value, t) ConditionalStyle
derived(v, t)Optional. Sieht alle Variants, für dimensionsübergreifende, stetige Logik (z. B. padding = f(size, Icon-Modus))
compoundsOptional. { matches(v), style(t) }, angewendet, wenn mehrere Dimensionen gleichzeitig zutreffen

Hier der echte Code aus examples/hello_button/styles.zig: Auch Apps können eigene Recipes definieren und eine Variante dann mit resolveBase zu einer Stilfunktion einfrieren, die ui.boxStyled akzeptiert.

Neben base hat ein ConditionalStyle sieben Bedingungs-Slots: selected, expanded, hover, active, focus, invalid, disabled. resolve(state) schichtet selected → expanded → hover → active → focus → invalid in dieser Reihenfolge; disabled schließt kurz, auf einem deaktivierten Node stapelt sich also nichts weiter.

resolve.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 });
SituationWerkzeug
Ein statischer StilBenannte Stilfunktion
Größen- / visuelle Variantenui.recipe.recipe
Mehrteilige Komponenten (Eingabefelder, Menüs)ui.recipe.slotRecipe
hover / disabled / invalidui.ConditionalStyle
Hell / dunkelEine vollständige Token-Palette; notfalls t.scheme lesen

Aktuelle Grenzen

  • ✓

    Komponenten lesen Tokens meist beim Mounten. Teile von ui.widgets.*, die keinen Hook zum erneuten Lesen der Tokens registrieren, behalten ihren Snapshot vom Mounten. Nach einem Theme-Wechsel zur Laufzeit werden styled Nodes der App erneut abgespielt, Nodes der Komponentenbibliothek erfordern aber eventuell einen Neuaufbau des Baums.

  • ✓

    Das erneute Abspielen umfasst den cx.root-Teilbaum. Das Overlay-Portal des Fensters liegt darin; von cx.root getrennte Bäume werden nicht aktualisiert.

  • ✓

    themeSignal folgt dem Scope, der es erzeugt hat. Erzeugen Sie es im Root-Scope der App, damit es alle Abonnenten überlebt.

Die Varianten- und Größenmatrix jeder Komponente finden Sie auf der Komponenten-Website.

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 5f9add5+wip 2026-09-30