docs/guide/components
Concepts clés · Components

Composants

Ce que promet un composant zenit, quand descendre d’une couche, et les trois contrats — montage, overlays et tests. Les enregistrements, props et types de résultat de chaque composant se trouvent sur le site des composants.

10 min de lecture

Ce que « composant » signifie ici

Un composant zenit n’est pas un helper qui assemble des couleurs et des rayons d’angle. Un composant public regroupe généralement la structure de nœuds, l’état persistant, le routage des événements, la sémantique du focus et du clavier, les propriétés d’accessibilité, les tokens de thème, la durée de vie des overlays et une frontière de test que le harness peut localiser et relire. Tous sont exportés depuis ui.widgets.

51Stories sur le site des composants
6Catégories d’usage (plus Labs)
1:1Story vers test_id E2E
ANATOMY
mount crée d’abord un Scope enfant ; tout ce qu’il possède est libéré avec le Scope parent.

Choisissez d’abord la bonne couche

BesoinCoucheReste à votre charge
Contrôles produit standardui.widgets.*État métier, textes, callbacks et placement
Visuel sur mesure, interaction éprouvéeui.hooks · ui.interaction · ui.select_headlessDessin, tokens, libellés a11y et frontières de composition
Un nouveau modèle d’interactionui.Node + ui.eventsHit-testing, clavier, focus, IME, a11y et contrat de test

Catalogue

Chaque composant a sa propre page sur le site des composants : un enregistrement du vrai zenit Storybook.app, les props et types de résultat générés depuis les sources, et ce que l’enregistrement démontre. Le harness localise chaque story par son test_id, pilote un curseur virtuel ou la saisie, et enregistre directement le drawable Metal.

CatégorieNombreExemples
Actions7Button · Checkbox · Switch · RadioGroup · Slider …
Affichage17Badge · Tag · Chip · Card · Alert …
Navigation4Tabs · Accordion · Menu · DropdownMenu
Surcouches5Notifier · Tooltip · Popover · Modal · Sheet
Mise en page8GlassBox · Divider · HStack / VStack · ui.box · VirtualList …
La story Button parcourt la matrice variant × size, puis survole et presse Primary. Détails sur /components/button.

Mount, état et nettoyage

La plupart des composants visuels utilisent la forme builder Config.mount(scope, cx) : ui.widgets.Button(props) renvoie un builder et c’est mount qui construit réellement l’arbre. La valeur de retour est soit le *Node racine, soit une struct de résultat avec des handles comme wrapper, trigger, body, panel et state.

mount_form.zig
const EditorActions = struct {
    document: *Document,

    fn save(self: *EditorActions) void {
        self.document.save();
    }
};

const bindings = try cx.bindState(EditorActions, .{ .document = document });

const save = try ui.widgets.Button(.{
    .label = "Save",
    .variant = .primary,
    .on_click = cx.on(EditorActions, bindings, EditorActions.save),
}).mount(scope, cx);

const name = try ui.widgets.Input(.{
    .label_text = "Project name",
    .placeholder = "Untitled",
    .required = true,
    .width = 320,
}).mount(scope, cx);

try form.appendChild(cx.allocator, name);
try form.appendChild(cx.allocator, save);

Quelques composants utilisent la forme fonction et reçoivent props, scope et cx en un seul appel : mountScrollArea, mountGrid, ainsi que Select, ComboBox, TagsInput, NumberStepper, FileUpload et DataTable (ce sont des alias de fonctions mountX dans ui.widgets, appelés sous la forme ui.widgets.Select(props, scope, cx)).

scroll_area.zig
// Function form: props, scope and cx in one call; returns a handle struct.
const area = try ui.widgets.mountScrollArea(.{ .height = 320 }, scope, cx);
try area.content.appendChild(cx.allocator, list);
try root.appendChild(cx.allocator, area.container);
ComposantRenvoie
Button · Input*Node
ModalModalResult{ overlay, dialog, body, portaled }
TooltipTooltipResult{ wrapper, trigger, content }
SelectSelectMount{ wrapper, trigger, panel, state, is_open }
mountScrollAreaScrollAreaResult{ container, content, state }
mountGridGridResult{ root, state }

Ne traitez pas la config comme des props vivantes que vous pourriez modifier plus tard. Un composant lit sa config au montage pour construire nœuds et état — un signal.get() dans la config n’est lu qu’une fois, comme instantané. Les données qui continuent de changer passent par le state renvoyé, un Signal (comme .visible(sig) de Modal) ou les méthodes publiques du composant ; ne conservez jamais de pointeur vers une config temporaire d’une frame à l’autre. L’état interne d’un composant vit sur le Scope enfant créé par mount et est libéré lors du dispose du Scope parent.

Pourquoi les overlays doivent être des composants

Tooltip, Popover, Menu, Modal, Sheet et le Notifier intégré à l’app enregistrent tous une couche auprès de l’OverlayStack. Les couches avec barrier (Modal / Sheet) sont rattachées au portal de la fenêtre (WindowOverlayPortal sous cx.root) afin d’échapper au clipping overflow de tout ancêtre. Les valeurs z proviennent de tiers sémantiques — overlay 100, dialog 1000, toast 2000 (là où vit le Notifier), tooltip 3000, l’overlay devtools étant fixé à 32000 — et un overlay ouvert depuis un autre passe toujours au-dessus de son hôte. Un positionnement absolu et un grand z-index sur un box ne vous donnent rien de tout cela.

modal.zig
const visible = try scope.createSignal(bool, false);

const modal = try ui.widgets.Modal(.{
    .title = "Delete document?",
    .width = 420,
}).visible(visible).mount(scope, cx);

try modal.body.appendChild(cx.allocator, confirm_content);
// modal.portaled == true: the barrier already lives under the window-level
// portal. Do not append modal.overlay to the current tree.

// Open it from any handler:
visible.set(true);
OVERLAY PORTAL
L’endroit où vous appelez mount ne décide pas de l’endroit où l’overlay est dessiné : la barrier va dans le portal, le tier décide du z, et Escape parcourt la pile depuis le haut jusqu’à la première couche refermable.
  • ✓

    Les clics internes restent dans le dialog / panel et ne déclenchent jamais d’outside-dismiss ; un clic sur la barrier le ferme selon close_on_overlay.

  • ✓

    Escape ne ferme que l’overlay du dessus ; les couches imbriquées se dépilent une à une.

  • ✓

    Focus : un Modal ouvert piège le focus et se focalise automatiquement, puis restaure le focus à sa position d’origine à la fermeture.

  • ✓

    Le dispose du Scope détache activement le sous-arbre de la barrier du portal : changer de page ne laisse jamais de couche de hit invisible.

  • ✓

    Les transitions de sortie gardent l’overlay visible jusqu’à la fin de l’animation, puis le suspendent — sans sauter directement à l’état final.

Pour les notifications globales non bloquantes, utilisez ui.widgets.Notifier (qui remplace l’ancien ToastManager) : Notifier.init(scope, cx, .{}) enregistre une couche sur le tier toast et, si le portal de la fenêtre existe, y monte son conteneur plein écran (si portaled vaut false, ajoutez vous-même container à votre racine). Ensuite show(.{ .tone = .success, .title = "Saved" }) renvoie un id, update transforme une carte sur place et dismiss la retire. Fermetures, expirations et actions de boutons reviennent à l’app sous forme d’événements Listener ; l’historique, le mode ne pas déranger et le reste de la logique de centre de notifications relèvent de l’app.

Le harness ouvre un dialog centré et vérifie que les clics internes ne le ferment pas. Modal
Un curseur virtuel survole le trigger ; la bulle entre dans le tier tooltip sans toucher au layout. Tooltip

Le showcase est aussi une suite de régression

e2e/storybook.test.ts passe à chaque story via nav.<key>, collecte le texte du sous-arbre et vérifie que chaque valeur de données attendue est présente, puis prend une capture ; les composants interactifs vérifient aussi l’état, la géométrie ou les pixels après interaction. Les enregistrements du site des composants réutilisent la même app et les mêmes ids sémantiques : la démo et le test ne divergent jamais en deux implémentations.

ContrôleCe qu’il détecte
Relecture texte / étatPanneaux vides, callbacks qui ne réécrivent jamais, valeurs de filtre / page erronées
Assertions de géométriePadding boxes, centrage dans le portal, alignement du Sheet au bord, deltas de glisser
Assertions de pixelsCouches GPU vides, régressions de blend, emoji en niveaux de gris, mauvais ordre z
EnregistrementsTiming du hover, du press, du curseur, de l’IME, du défilement et des animations d’entrée
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