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.
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 :
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
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.
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 — 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;
};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.
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)
brand_dark
ui.theme.dark (par défaut)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 :
fg_primary · fg_secondary · fg_tertiary · fg_disabledPresque tous les textes et icônesbg_primary · bg_secondary · bg_tertiaryPanneaux, cartes, Calendar, Chip, pistes de Progress / Slider, Skeletonbg_hover · bg_activeÉtats survol et pressé des contrôles cliquablesaccentCheckbox / Switch cochés, liens, Tabs, Breadcrumb, dates sélectionnées, Badgebutton_primary_bg · button_primary_fgButton primary (hover / pressé dérivés du fond) ; fg sert aussi au texte inversé sur Badge, Chip et Tabsborder · border_strong · separatorBordures et séparateursborder_focus · input_bg · input_border · selection_bgInput, Select, NumberStepper, DatePicker et autres champssuccess · warning · danger · info · status_fgAlert, Badge, Tag, Progress, Button dangerlist_hover_bg · list_selection_bgMenu, DropdownMenu, ComboBox, Table, Tree, DataTablecheckbox_* · switch_thumb · switch_track_offCheckbox / Switch non cochés et désactivéstooltip_bg · tooltip_fg · overlay · scrollbar_thumbTooltip, voile de Modal / Sheet, barres de défilementQuelques 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 :
xs2.5 × 2 + 12 × 1.25 = 20sm4.5 × 2 + 12 × 1.25 = 24md7.25 × 2 + 14 × 1.25 = 32lg10 × 2 + 16 × 1.25 = 40Pour 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.
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),// 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;Avant de livrer
- ✓
schemecorrespond au fond (un fond sombre doit être.dark). - ✓
button_primary_fgsurbutton_primary_bgetstatus_fgsurdangeratteignent au moins 4.5:1 — le même contrôle que les tests unitaires des presets intégrés. - ✓
borderreste distinguable debg_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).