Styles et thèmes
Utilisez les tokens, les fonctions de style nommées et les recipes pour que le visuel et la logique métier restent indépendants.
Le modèle à trois couches
La source unique des couleurs, tailles de texte, espacements, rayons et dimensions des contrôles.
Des fonctions pures que vous pouvez nommer, réutiliser, tester — et rejouer quand le thème change.
Gère les variantes, les conditions d'interaction et les composants en plusieurs parties.
Les tokens d'abord
Lisez les valeurs de style depuis cx.tokens (ou le t que reçoit une fonction de style). Pour les valeurs de design réellement hors échelle, marquez-les avec ui.arb : c'est une identité sans coût à l'exécution, et sa valeur est sémantique — le lecteur distingue une valeur arbitraire intentionnelle d'un littéral paresseux qui devrait devenir un token.
// 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),Fonctions de style nommées
Une fonction de style a la signature fn (*const ui.ThemeTokens) ui.BoxStyle (ou ui.TextStyle). Regroupez-les dans styles.zig et laissez les vues y faire référence par leur nom.
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 et vstackStyled enregistrent sur le nœud un hook on_theme qui mémorise la fonction de style elle-même. Quand le thème change, le nœud recalcule son style à partir des nouveaux tokens ; les tokens lus dans un ui.box ordinaire ne sont qu'un instantané pris au montage.
Changer de thème
Trois thèmes sont fournis : ui.theme.light, ui.theme.dark et ui.theme.high_contrast. cx.setTheme remplace le pointeur de tokens, parcourt le sous-arbre cx.root en rejouant les hooks on_theme et before_render de chaque nœud, puis les marque comme sales. Pour un accès réactif au thème courant, utilisez 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);Les différences clair/sombre relèvent de la palette de tokens complète ; ne lisez t.scheme (.light / .dark) dans une fonction de style que si vous avez réellement besoin d'une branche.
Pour appliquer votre couleur de marque ou créer vos propres thèmes clair / sombre, consultez Thèmes personnalisés.
Recipes et conditions
ui.recipe est un espace de noms : ui.recipe.recipe(Config) définit une recipe à nœud unique et ui.recipe.slotRecipe(Config) une recipe en plusieurs parties. La résolution fusionne base → variants → derived → compounds, les champs non nuls ultérieurs l'emportant ; l'override de style externe du composant est appliqué en dernier.
VariantsObligatoire. Chaque champ est une dimension de variante (enum ou bool) avec une valeur par défautbase(t)Facultatif. Le ConditionalStyle de basevariantsFacultatif. Un resolver par dimension : fn (value, t) ConditionalStylederived(v, t)Facultatif. Voit toutes les Variants, pour une logique continue entre dimensions (p. ex. padding = f(size, mode icône))compoundsFacultatif. { matches(v), style(t) } appliqué quand plusieurs dimensions correspondent ensembleVoici le code réel de examples/hello_button/styles.zig : les apps peuvent aussi définir leurs propres recipes, puis figer une variante avec resolveBase en une fonction de style acceptée par ui.boxStyled.
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;
}En plus de base, un ConditionalStyle porte sept emplacements de condition : selected, expanded, hover, active, focus, invalid, disabled. resolve(state) superpose selected → expanded → hover → active → focus → invalid dans cet ordre ; disabled court-circuite, donc rien d'autre ne s'empile sur un nœud désactivé.
// 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 si nécessaireLimites actuelles
- ✓
Les composants lisent surtout les tokens au montage. Les parties de
ui.widgets.*qui n'enregistrent pas de hook pour relire les tokens conservent leur instantané du montage. Après un changement de thème à l'exécution, les nœuds styled de l'app sont rejoués, mais les nœuds de la bibliothèque de composants peuvent nécessiter une reconstruction de l'arbre. - ✓
Le rejeu couvre le sous-arbre cx.root. Le portal d'overlay de la fenêtre en fait partie ; les arbres détachés de
cx.rootne sont pas mis à jour. - ✓
themeSignal suit le Scope qui l'a créé. Créez-le sur le Scope racine de l'app pour qu'il survive à tous ses abonnés.
Consultez le site des composants pour la matrice de variantes et de tailles de chaque composant.