Styling und Themes
Mit Tokens, benannten Stilfunktionen und Recipes bleiben Optik und Geschäftslogik voneinander unabhängig.
Das Drei-Schichten-Modell
Die einzige Quelle für Farben, Schriftgrößen, Abstände, Radien und Control-Maße.
Reine Funktionen, die Sie benennen, wiederverwenden, testen — und beim Theme-Wechsel erneut ausführen können.
Behandelt Varianten, Interaktionszustände und mehrteilige Komponenten.
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.
// 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.
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,
};
}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).
// 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.
VariantsPflicht. Jedes Feld ist eine Variantendimension (enum oder bool) mit Standardwertbase(t)Optional. Der Basis-ConditionalStylevariantsOptional. Ein Resolver pro Dimension: fn (value, t) ConditionalStylederived(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 zutreffenHier 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.
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;
}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.
// 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.recipeui.recipe.slotRecipeui.ConditionalStylet.scheme lesenAktuelle 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.rootgetrennte 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.