---
title: "Thèmes personnalisés — Docs zenit Zig UI"
description: "Un thème est une seule valeur ThemeTokens : couleurs, espacements, rayons, tailles de texte et dimensions des contrôles."
url: https://zenit.z.express/fr/docs/guide/theming
language: fr
alternate_en: https://zenit.z.express/docs/guide/theming.md
alternate_zh: https://zenit.z.express/zh/docs/guide/theming.md
alternate_es: https://zenit.z.express/es/docs/guide/theming.md
alternate_ja: https://zenit.z.express/ja/docs/guide/theming.md
alternate_ko: https://zenit.z.express/ko/docs/guide/theming.md
alternate_de: https://zenit.z.express/de/docs/guide/theming.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# Thèmes personnalisés

Un thème est une seule valeur ThemeTokens : couleurs, espacements, rayons, tailles de texte et dimensions des contrôles. Copiez un preset intégré, modifiez les champs qui portent votre marque, installez-le avec cx.setTheme, et c'est votre thème.

Cette page explique comment définir et installer un thème. La façon dont les vues consomment les tokens — fonctions de style et recipes — est traitée dans [Styles et thèmes](https://zenit.z.express/fr/docs/guide/styling).

## Le thème par défaut

Le `Cx` de chaque fenêtre démarre sur `ui.theme.dark`. Une app qui n'appelle jamais `setTheme` est sombre — c'est le cas de Hello Button. Pour utiliser un autre thème, définissez-le à la première ligne de votre fonction de montage, avant qu'aucun nœud n'existe :

`main.zig`

```zig
fn mountUI(cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node {
    // Cx starts on ui.theme.dark. Pick the theme before building any node,
    // so every component reads the right tokens when it mounts.
    cx.setTheme(&ui.theme.light);

    const root = try ui.boxStyled(cx, S.root, .{});
    // …
    return root;
}
```

Avec plusieurs fenêtres, chacune a son propre `Cx` : définissez donc le thème dans chaque fonction de montage. zenit **ne suit pas** encore l'apparence du système : un changement clair/sombre du système ne déclenche qu'un redessin, et aucune API publique ne permet de lire l'apparence du système. Proposez plutôt un interrupteur à vos utilisateurs (voir « Changer à l'exécution »).

## Anatomie de ThemeTokens

`src/ui/theme.zig (extrait)`

```zig
pub const ThemeTokens = struct {
    name: []const u8 = "unnamed",
    scheme: ColorScheme = .light,      // .light / .dark — the one flag for "is this dark?"
    color: ColorTokens,                // 54 colors, no defaults: you must provide all of them
    space: SpaceScale = .{},           // _0 … _12  (0 … 48 px, 4 px grid)
    radius: RadiusScale = .{},         // none sm md lg xl full
    font_size: FontSizeScale = .{},    // xxs … xxxl (9 … 24)
    border_width: BorderWidthScale = .{},
    size: SizeTokens = .{},            // checkbox, switch, badge, dot, close button
    control: ControlScale = .{},       // xs / sm / md / lg metrics for every control
    duration: DurationTokens = .{},    // fast / normal / slow (seconds)
    shadow: ShadowTokens = .{},        // sm / md / lg
};
```

Tout sauf `color` a des valeurs par défaut. Les 54 couleurs de `ColorTokens` n'en ont aucune : n'écrivez donc pas un littéral de struct à partir de zéro — copiez `ui.theme.light`, `ui.theme.dark` ou `ui.theme.high_contrast` et ne modifiez que ce qui vous intéresse.

> NOTE
> 
> **Utilisez scheme pour distinguer clair et sombre.** `scheme` est le seul indicateur clair/sombre : le Notifier choisit sa palette d'après lui et DevTools en dérive ses couleurs. Ne cherchez jamais « dark » dans `name` — le thème à contraste élevé est noir pur et son nom n'en dit rien.

## Dériver un thème de marque

Écrivez les thèmes comme des `const` au niveau du conteneur ; le bloc d'initialisation s'exécute à la compilation et le résultat a un stockage statique, donc le pointeur que conserve `cx.setTheme(&brand)` reste valide pendant toute la vie du programme. Ne prenez jamais l'adresse d'un thème stocké dans une variable locale de fonction.

`theme.zig`

```zig
// theme.zig — your app's themes. Container-level consts: they are
// evaluated at compile time and live for the whole program, which is
// what cx.setTheme(&brand) needs (it keeps the pointer).
const ui = @import("ui");

pub const brand: ui.ThemeTokens = blk: {
    var t = ui.theme.light; // copy a complete preset (every color is required)
    t.name = "brand";
    t.scheme = .light;

    const blue = ui.Color.hex(0x1144AA);
    t.color.accent = blue;                       // links, checked states, active tabs…
    t.color.accent_hover = ui.Color.hex(0x0D3688);
    t.color.accent_subtle = ui.Color.hex(0xE6ECF7);
    t.color.button_primary_bg = blue;            // primary buttons read these two,
    t.color.button_primary_fg = ui.Color.WHITE;  //   not accent
    t.color.border_focus = blue;
    t.color.selection_bg = ui.Color.hex(0xC9D6F0);
    t.color.list_selection_bg = ui.Color.hex(0xE6ECF7);

    t.radius.lg = 10;
    t.control.md.radius = 10;
    t.control.md.padding_h = 14;
    break :blk t;
};
```

> WARNING
> 
> **Les tokens de composants ne suivent pas accent.** Dans les presets, `accent` et `button_primary_bg` sont des valeurs indépendantes — rien n'est dérivé. Quand vous changez la couleur de marque, modifiez ensemble `accent`, `button_primary_bg` / `button_primary_fg` et `border_focus`, sinon les boutons primary gardent la couleur du preset.

## Un thème sombre assorti

Dérivez la variante sombre de `ui.theme.dark` plutôt que de recolorer le thème de marque clair : les dizaines de valeurs de fond, de texte et de bordure du preset sombre sont réglées ensemble. Sur fond sombre, la couleur de marque doit généralement être éclaircie, et le texte des boutons primary devient sombre.

`theme.zig`

```zig
pub const brand_dark: ui.ThemeTokens = blk: {
    var t = ui.theme.dark; // start from the dark preset, not from brand
    t.name = "brand_dark";
    t.scheme = .dark;      // Notification palettes and DevTools key off this

    const blue = ui.Color.hex(0x7FA2F0); // lighter on dark backgrounds
    t.color.accent = blue;
    t.color.button_primary_bg = blue;
    t.color.button_primary_fg = ui.Color.hex(0x0D1119);
    t.color.border_focus = blue;
    break :blk t;
};
```

![brand (clair)](https://zenit.z.express/media/theme-brand.webp?v=588f003545)brand (clair)

![brand_dark](https://zenit.z.express/media/theme-brand-dark.webp?v=854df7b29f)brand\_dark

![ui.theme.dark (par défaut)](https://zenit.z.express/media/theme-default-dark.webp?v=3b5b953dc1)ui.theme.dark (par défaut)

Le même Hello Button, seul l'argument de setTheme change — les trois captures proviennent de la vraie fenêtre, prises par le harness E2E.

## Quels tokens pilotent quoi

Regroupés selon ce que lisent réellement les composants (sources v0.1.0-alpha). Commencez par ceux-ci pour créer un thème :

| Token | Principalement utilisé par |
| --- | --- |
| `fg_primary · fg_secondary · fg_tertiary · fg_disabled` | Presque tous les textes et icônes |
| `bg_primary · bg_secondary · bg_tertiary` | Panneaux, cartes, Calendar, Chip, pistes de Progress / Slider, Skeleton |
| `bg_hover · bg_active` | États survol et pressé des contrôles cliquables |
| `accent` | Checkbox / Switch cochés, liens, Tabs, Breadcrumb, dates sélectionnées, Badge |
| `button_primary_bg · button_primary_fg` | Button primary (hover / pressé dérivés du fond) ; fg sert aussi au texte inversé sur Badge, Chip et Tabs |
| `border · border_strong · separator` | Bordures et séparateurs |
| `border_focus · input_bg · input_border · selection_bg` | Input, Select, NumberStepper, DatePicker et autres champs |
| `success · warning · danger · info · status_fg` | Alert, Badge, Tag, Progress, Button danger |
| `list_hover_bg · list_selection_bg` | Menu, DropdownMenu, ComboBox, Table, Tree, DataTable |
| `checkbox_* · switch_thumb · switch_track_off` | Checkbox / Switch non cochés et désactivés |
| `tooltip_bg · tooltip_fg · overlay · scrollbar_thumb` | Tooltip, voile de Modal / Sheet, barres de défilement |

> WARNING
> 
> **Aucun composant ne lit encore ces tokens.** `accent_muted`, `bg_inset`, `button_secondary_bg`, `button_secondary_fg`, `switch_track_on` (un Switch activé utilise `accent`), `table_header_bg`, `list_selection_hover_bg`, ainsi que `success_subtle` / `warning_subtle` / `danger_subtle` / `info_subtle`. Les modifier n'a aujourd'hui aucun effet visible ; vos propres fonctions de style peuvent toujours les utiliser.

Quelques composants ont leurs propres couleurs au lieu de lire `ColorTokens` : le Notifier choisit entre ses palettes claire et sombre intégrées selon `scheme`, et les teintes de GlassBox ainsi que certains détails de Progress et Select sont des valeurs fixes. Ils suivent le mode clair/sombre, mais pas votre couleur de marque.

## Rayons, espacements et dimensions des contrôles

Les tokens hors couleur peuvent être surchargés champ par champ. Le plus utile est `control` : Button, Input, Select, ComboBox, DatePicker, DateRangePicker, Tabs et Chip partagent quatre jeux de dimensions, xs / sm / md / lg. La hauteur d'un contrôle n'est pas un token — elle est calculée :

| Taille | Hauteur par défaut = padding\_y × 2 + font\_size × line\_height |
| --- | --- |
| `xs` | 2.5 × 2 + 12 × 1.25 = 20 |
| `sm` | 4.5 × 2 + 12 × 1.25 = 24 |
| `md` | 7.25 × 2 + 14 × 1.25 = 32 |
| `lg` | 10 × 2 + 16 × 1.25 = 40 |

Pour rendre les contrôles md plus hauts, modifiez donc `t.control.md.padding_y` ou la taille de police, et tous les contrôles md suivent ; ne codez pas en dur la `height` d'un contrôle. Gardez `icon_size` inférieur ou égal à la hauteur de ligne.

## Changer à l'exécution

`cx.setTheme` peut être appelé à tout moment : il remplace le pointeur de tokens, rejoue les hooks `on_theme` / `before_render` sur tout le sous-arbre `cx.root` (portal d'overlay compris) et demande un redessin.

`prefs.zig`

```zig
const Prefs = struct {
    cx: *ui.Cx,
    dark: bool = false,

    pub fn toggle(self: *Prefs) void {
        self.dark = !self.dark;
        self.cx.setTheme(if (self.dark) &themes.brand_dark else &themes.brand);
    }
};

// In mountUI:
const prefs = try cx.bindState(Prefs, .{ .cx = cx });
cx.setTheme(&themes.brand);
// …
.on_click = cx.on(Prefs, prefs, Prefs.toggle),
```

```zig
// A reactive read of the current theme (lives as long as `scope`).
const theme_sig = try cx.themeSignal(scope);
const is_dark = theme_sig.get().scheme == .dark;
```

> NOTE
> 
> **Ce qu'atteint un changement à l'exécution.** Les nœuds construits avec `boxStyled` / `textStyled` et les composants qui enregistrent des hooks de thème changent de couleur immédiatement ; la plupart des `ui.widgets.*` lisent les tokens au montage, reconstruisez donc ces sous-arbres pour un changement complet. Les apps qui choisissent leur thème une fois au démarrage ne sont pas concernées.

## Avant de livrer

-   `scheme` correspond au fond (un fond sombre doit être `.dark`).
    
-   `button_primary_fg` sur `button_primary_bg` et `status_fg` sur `danger` atteignent au moins 4.5:1 — le même contrôle que les tests unitaires des presets intégrés.
    
-   `border` reste distinguable de `bg_primary` (les tests des presets exigent ≥ 1.2).
    
-   Passez le Storybook sur votre thème et parcourez les composants courants (la story Component Wall est un bon point de départ).
