---
title: "Styles et thèmes — Docs zenit Zig UI"
description: "Utilisez les tokens, les fonctions de style nommées et les recipes pour que le visuel et la logique métier restent indépendants."
url: https://zenit.z.express/fr/docs/guide/styling
language: fr
alternate_en: https://zenit.z.express/docs/guide/styling.md
alternate_zh: https://zenit.z.express/zh/docs/guide/styling.md
alternate_es: https://zenit.z.express/es/docs/guide/styling.md
alternate_ja: https://zenit.z.express/ja/docs/guide/styling.md
alternate_ko: https://zenit.z.express/ko/docs/guide/styling.md
alternate_de: https://zenit.z.express/de/docs/guide/styling.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# 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

**01 · ThemeTokens**

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

**02 · styles.zig**

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

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

[Video](https://zenit.z.express/media/stories/button.mp4?v=6203a01415)

La matrice de Button : variant × size, icône, loading, disabled et block, stylée via une recipe.

[Video](https://zenit.z.express/media/stories/glassbox.mp4?v=570227ca3d)

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

> WARNING
> 
> **Évitez les littéraux bruts.** Les valeurs `Color.hex(...)` et `font_size` fixes dans les vues contournent le thème. `scripts/check_style_literals.sh` signale les littéraux bruts et laisse passer `ui.arb` : un grep sur `ui.arb` inventorie donc toutes les valeurs hors token. Dès que la même valeur arb apparaît trois fois ou plus, envisagez d'en faire un token.

## 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`

```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`

```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`

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

> NOTE
> 
> **Le rejeu couvre le sous-arbre cx.root.** Les overlays des composants (Modal, Popover, Menu, Tooltip) vivent dans le `WindowOverlayPortal` sous `cx.root`, donc `setTheme` les rejoue aussi ; seuls les arbres de nœuds détachés de `cx.root` sont hors de portée. Ce qui est rejoué, ce sont les hooks enregistrés par les nœuds styled — la plupart des `ui.widgets.*` figent les tokens au montage et doivent encore être reconstruits.

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](https://zenit.z.express/fr/docs/guide/theming).

## 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 Config | Rôle |
| --- | --- |
| `Variants` | Obligatoire. Chaque champ est une dimension de variante (enum ou bool) avec une valeur par défaut |
| `base(t)` | Facultatif. Le ConditionalStyle de base |
| `variants` | Facultatif. 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)) |
| `compounds` | Facultatif. { 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`.

`examples/hello_button/styles.zig`

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

`resolve.zig`

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

| Situation | Outil |
| --- | --- |
| Un style statique | Fonction de style nommée |
| Variantes de taille / visuelles | `ui.recipe.recipe` |
| Composants en plusieurs parties (champs, menus) | `ui.recipe.slotRecipe` |
| hover / disabled / invalid | `ui.ConditionalStyle` |
| Clair / sombre | Une 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](https://zenit.z.express/fr/components) pour la matrice de variantes et de tailles de chaque composant.
