---
title: "Componentes — Docs de zenit Zig UI"
description: "Qué promete un componente de zenit, cuándo bajar de capa y los tres contratos: montaje, overlays y pruebas."
url: https://zenit.z.express/es/docs/guide/components
language: es
alternate_en: https://zenit.z.express/docs/guide/components.md
alternate_zh: https://zenit.z.express/zh/docs/guide/components.md
alternate_ja: https://zenit.z.express/ja/docs/guide/components.md
alternate_ko: https://zenit.z.express/ko/docs/guide/components.md
alternate_fr: https://zenit.z.express/fr/docs/guide/components.md
alternate_de: https://zenit.z.express/de/docs/guide/components.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# Componentes

Qué promete un componente de zenit, cuándo bajar de capa y los tres contratos: montaje, overlays y pruebas. Las grabaciones, props y tipos de resultado de cada componente están en el [sitio de componentes](https://zenit.z.express/es/components).

## Qué significa aquí “componente”

Un componente de zenit no es un helper que junta colores y radios de esquina. Un componente público suele empaquetar estructura de nodos, estado persistente, enrutamiento de eventos, semántica de foco y teclado, propiedades de accesibilidad, tokens de tema, ciclo de vida de overlays y un límite de prueba que el harness puede localizar y releer. Todos se exportan desde `ui.widgets`.

**51**Stories en el sitio de componentes

**6**Categorías de uso (más Labs)

**1:1**Story a test\_id de E2E

ANATOMY

mount crea primero un Scope hijo; todo lo que posee se libera junto con el Scope padre.

## Elige primero la capa correcta

| Necesidad | Capa | Sigues a cargo de |
| --- | --- | --- |
| Controles de producto estándar | `ui.widgets.*` | Estado de negocio, textos, callbacks y ubicación |
| Visual propio, interacción probada | `ui.hooks` · `ui.interaction` · `ui.select_headless` | Dibujo, tokens, etiquetas a11y y límites de composición |
| Un nuevo patrón de interacción | `ui.Node` + `ui.events` | Hit-testing, teclado, foco, IME, a11y y el contrato de pruebas |

> TIP
> 
> **Empieza con widgets.** Baja de capa solo cuando el modelo de comportamiento de un componente existente no encaje. Un aspecto distinto rara vez justifica reimplementar foco, IME, cierre de overlays y accesibilidad.

## Catálogo

Cada componente tiene su propia página en el [sitio de componentes](https://zenit.z.express/es/components): una grabación del `zenit Storybook.app` real, props y tipos de resultado generados desde el código fuente, y lo que la grabación demuestra. El harness localiza cada story por `test_id`, maneja un cursor virtual o la entrada y graba directamente el drawable de Metal.

| Categoría | Cantidad | Ejemplos |
| --- | --- | --- |
| Acciones | 7 | [Button](https://zenit.z.express/es/components/button) · [Checkbox](https://zenit.z.express/es/components/checkbox) · [Switch](https://zenit.z.express/es/components/switch) · [RadioGroup](https://zenit.z.express/es/components/radio) · [Slider](https://zenit.z.express/es/components/slider) … |
| Entradas | 10 | [Input](https://zenit.z.express/es/components/input) · [Textarea](https://zenit.z.express/es/components/textarea) · [Select](https://zenit.z.express/es/components/select) · [Input · Select · Button](https://zenit.z.express/es/components/formcompose) · [ComboBox](https://zenit.z.express/es/components/combobox) … |
| Visualización | 17 | [Badge](https://zenit.z.express/es/components/badge) · [Tag](https://zenit.z.express/es/components/tag) · [Chip](https://zenit.z.express/es/components/chip) · [Card](https://zenit.z.express/es/components/card) · [Alert](https://zenit.z.express/es/components/alert) … |
| Navegación | 4 | [Tabs](https://zenit.z.express/es/components/tabs) · [Accordion](https://zenit.z.express/es/components/accordion) · [Menu](https://zenit.z.express/es/components/menu) · [DropdownMenu](https://zenit.z.express/es/components/dropdown) |
| Superposiciones | 5 | [Notifier](https://zenit.z.express/es/components/notification) · [Tooltip](https://zenit.z.express/es/components/tooltip) · [Popover](https://zenit.z.express/es/components/popover) · [Modal](https://zenit.z.express/es/components/modal) · [Sheet](https://zenit.z.express/es/components/sheet) |
| Layout | 8 | [GlassBox](https://zenit.z.express/es/components/glassbox) · [Divider](https://zenit.z.express/es/components/divider) · [HStack / VStack](https://zenit.z.express/es/components/stack) · [ui.box](https://zenit.z.express/es/components/layoutbox) · [VirtualList](https://zenit.z.express/es/components/virtuallist) … |
| Labs | 13 | [glasslab](https://zenit.z.express/es/components/glasslab) · [glassislands](https://zenit.z.express/es/components/glassislands) · [glassmotion](https://zenit.z.express/es/components/glassmotion) · [glasschrome](https://zenit.z.express/es/components/glasschrome) · [canvasevents](https://zenit.z.express/es/components/canvasevents) … |

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

La story de Button recorre la matriz variant × size y luego hace hover y press sobre Primary. Detalles en [/components/button](https://zenit.z.express/es/components/button).

## Mount, estado y limpieza

La mayoría de los componentes visuales usan la forma builder `Config.mount(scope, cx)`: `ui.widgets.Button(props)` devuelve un builder y `mount` es quien construye el árbol. El valor de retorno es el `*Node` raíz o un struct de resultado con handles como wrapper, trigger, body, panel y state.

`mount_form.zig`

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

Unos pocos componentes usan la forma de función y reciben props, scope y cx en una sola llamada: `mountScrollArea`, `mountGrid`, además de `Select`, `ComboBox`, `TagsInput`, `NumberStepper`, `FileUpload` y `DataTable` (son alias de funciones `mountX` en `ui.widgets` y se llaman como `ui.widgets.Select(props, scope, cx)`).

`scroll_area.zig`

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

| Componente | Devuelve |
| --- | --- |
| `Button` · `Input` | `*Node` |
| `Modal` | `ModalResult{ overlay, dialog, body, portaled }` |
| `Tooltip` | `TooltipResult{ wrapper, trigger, content }` |
| `Select` | `SelectMount{ wrapper, trigger, panel, state, is_open }` |
| `mountScrollArea` | `ScrollAreaResult{ container, content, state }` |
| `mountGrid` | `GridResult{ root, state }` |

No trates la config como props vivas en las que puedas escribir después. Un componente lee su config al montarse para construir nodos y estado: un `signal.get()` dentro de la config se lee una sola vez, como instantánea. Los datos que siguen cambiando entran a través del state devuelto, un Signal (como `.visible(sig)` de Modal) o los métodos públicos del componente; nunca guardes un puntero a una config temporal entre frames. El estado interno de un componente vive en el Scope hijo que crea mount y se libera cuando se hace dispose del Scope padre.

## Por qué los overlays deben ser componentes

Tooltip, Popover, Menu, Modal, Sheet y el [Notifier](https://zenit.z.express/es/components/notification) integrado en la app registran una capa en el `OverlayStack`. Las capas con barrier (Modal / Sheet) se reubican en el portal a nivel de ventana (`WindowOverlayPortal` bajo `cx.root`) para escapar del recorte por overflow de cualquier ancestro. Los valores z salen de tiers semánticos — overlay 100, dialog 1000, toast 2000 (donde vive el Notifier), tooltip 3000, con el overlay de devtools fijo en 32000 — y un overlay abierto desde otro siempre queda por encima de su anfitrión. Posicionamiento absoluto y un z-index alto en un box no te dan nada de esto.

`modal.zig`

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

Dónde llamas a mount no decide dónde se dibuja el overlay: el barrier va al portal, el tier decide la z y Escape recorre la pila desde arriba hasta la primera capa que se pueda cerrar.

-   **Los clics internos** se quedan dentro del dialog / panel y nunca disparan outside-dismiss; hacer clic en el barrier lo cierra según `close_on_overlay`.
    
-   **Escape** cierra solo el overlay superior; las capas anidadas se deshacen de una en una.
    
-   **Foco**: un Modal abierto atrapa el foco y lo enfoca automáticamente; al cerrarse, lo devuelve a donde estaba.
    
-   **El dispose del Scope** separa activamente el subárbol del barrier del portal, así que cambiar de página nunca deja una capa de impacto invisible.
    
-   **Las transiciones de salida** mantienen el overlay visible hasta que la animación termina y luego lo suspenden, sin saltar al estado final.
    

Para notificaciones globales no bloqueantes usa `ui.widgets.Notifier` (reemplaza al antiguo ToastManager): `Notifier.init(scope, cx, .{})` registra una capa en el tier toast y, si existe el portal de la ventana, monta ahí su contenedor de ventana completa (si `portaled` es false, añade tú mismo `container` a tu raíz). Luego `show(.{ .tone = .success, .title = "Saved" })` devuelve un id, `update` transforma una tarjeta en su sitio y `dismiss` la retira. Los cierres, vencimientos y acciones de botones vuelven a la app como eventos `Listener`; el historial, el modo no molestar y demás lógica del centro de notificaciones son cosa de la app.

[Video](https://zenit.z.express/media/stories/modal.mp4?v=f1b080996c)

El harness abre un dialog centrado y verifica que los clics internos no lo cierran. [Modal](https://zenit.z.express/es/components/modal)

[Video](https://zenit.z.express/media/stories/tooltip.mp4?v=64c28daff2)

Un cursor virtual pasa sobre el trigger; la burbuja entra en el tier tooltip sin tocar el layout. [Tooltip](https://zenit.z.express/es/components/tooltip)

## El showcase también es una suite de regresión

`e2e/storybook.test.ts` cambia a cada story mediante `nav.<key>`, recoge el texto del subárbol y comprueba que aparece cada valor de datos esperado, y luego hace una captura; los componentes interactivos también comprueban estado, geometría o píxeles tras interactuar. Las grabaciones del sitio de componentes reutilizan la misma app y los mismos ids semánticos, así que la demo y la prueba nunca se bifurcan en dos implementaciones.

| Control | Qué detecta |
| --- | --- |
| Relectura de texto / estado | Paneles vacíos, callbacks que nunca escriben de vuelta, valores de filtro / página erróneos |
| Aserciones de geometría | Padding boxes, centrado en el portal, alineación de Sheet al borde, desplazamientos de arrastre |
| Aserciones de píxeles | Capas GPU en blanco, regresiones de blend, emoji en escala de grises, orden z incorrecto |
| Grabaciones | Tiempos de hover, press, cursor, IME, scroll y animaciones de entrada |

> WARNING
> 
> **Storybook no son tus componentes de negocio.** Editores, exploradores de archivos, paletas de comandos y modelos de dominio pertenecen a la app. La biblioteca pública solo promete comportamiento reutilizable y primitivas visuales.
