docs/guide/styling
Concepts clés · Design system

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.

12 min de lecture

Le modèle à trois couches

01
01 · ThemeTokens

La source unique des couleurs, tailles de texte, espacements, rayons et dimensions des contrôles.

02
02 · styles.zig

Des fonctions pures que vous pouvez nommer, réutiliser, tester — et rejouer quand le thème change.

03
03 · Recipe

Gère les variantes, les conditions d'interaction et les composants en plusieurs parties.

STYLE LAYERS
Chaque couche ne lit que celle du dessous : les vues ne codent jamais une couleur en dur, donc changer de thème revient à remplacer uniquement la couche du bas.
La matrice de Button : variant × size, icône, loading, disabled et block, stylée via une recipe.
GlassBox en modes regular, interactive et clear avec un vrai backdrop blur.

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.

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

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.

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

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

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.

RECIPE MERGE
Chaque étape n'écrase que les champs qu'elle définit. Par convention, derived ne produit que de la géométrie (padding, rayon, taille) et ne touche jamais background.
Membre de ConfigRôle
VariantsObligatoire. Chaque champ est une dimension de variante (enum ou bool) avec une valeur par défaut
base(t)Facultatif. Le ConditionalStyle de base
variantsFacultatif. Un resolver par dimension : fn (value, t) ConditionalStyle
derived(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 ensemble

Voici 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.

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é.

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 });
SituationOutil
Un style statiqueFonction de style nommée
Variantes de taille / visuellesui.recipe.recipe
Composants en plusieurs parties (champs, menus)ui.recipe.slotRecipe
hover / disabled / invalidui.ConditionalStyle
Clair / sombreUne palette de tokens complète ; lisez t.scheme si nécessaire

Limites 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.root ne 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.

zenit · Double licenceGratuit pour les projets open source sous GPL-3.0-only ; les produits propriétaires ou commerciaux nécessitent une licence commerciale.Contacter l’auteur : zongyi.xzy#gmail.com (remplacez # par @)zenit 5f9add5+wip 2026-09-30