---
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://ui.surf/zenit/es/docs/guide/components
language: es
alternate_en: https://ui.surf/zenit/docs/guide/components.md
alternate_zh: https://ui.surf/zenit/zh/docs/guide/components.md
alternate_ja: https://ui.surf/zenit/ja/docs/guide/components.md
alternate_ko: https://ui.surf/zenit/ko/docs/guide/components.md
alternate_fr: https://ui.surf/zenit/fr/docs/guide/components.md
alternate_de: https://ui.surf/zenit/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://ui.surf/zenit/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://ui.surf/zenit/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://ui.surf/zenit/es/components/button) · [Checkbox](https://ui.surf/zenit/es/components/checkbox) · [Switch](https://ui.surf/zenit/es/components/switch) · [RadioGroup](https://ui.surf/zenit/es/components/radio) · [Slider](https://ui.surf/zenit/es/components/slider) … |
| Entradas | 10 | [Input](https://ui.surf/zenit/es/components/input) · [Textarea](https://ui.surf/zenit/es/components/textarea) · [Select](https://ui.surf/zenit/es/components/select) · [Input · Select · Button](https://ui.surf/zenit/es/components/formcompose) · [ComboBox](https://ui.surf/zenit/es/components/combobox) … |
| Visualización | 17 | [Badge](https://ui.surf/zenit/es/components/badge) · [Tag](https://ui.surf/zenit/es/components/tag) · [Chip](https://ui.surf/zenit/es/components/chip) · [Card](https://ui.surf/zenit/es/components/card) · [Alert](https://ui.surf/zenit/es/components/alert) … |
| Navegación | 4 | [Tabs](https://ui.surf/zenit/es/components/tabs) · [Accordion](https://ui.surf/zenit/es/components/accordion) · [Menu](https://ui.surf/zenit/es/components/menu) · [DropdownMenu](https://ui.surf/zenit/es/components/dropdown) |
| Superposiciones | 5 | [Notifier](https://ui.surf/zenit/es/components/notification) · [Tooltip](https://ui.surf/zenit/es/components/tooltip) · [Popover](https://ui.surf/zenit/es/components/popover) · [Modal](https://ui.surf/zenit/es/components/modal) · [Sheet](https://ui.surf/zenit/es/components/sheet) |
| Layout | 8 | [GlassBox](https://ui.surf/zenit/es/components/glassbox) · [Divider](https://ui.surf/zenit/es/components/divider) · [HStack / VStack](https://ui.surf/zenit/es/components/stack) · [ui.box](https://ui.surf/zenit/es/components/layoutbox) · [VirtualList](https://ui.surf/zenit/es/components/virtuallist) … |
| Labs | 13 | [glasslab](https://ui.surf/zenit/es/components/glasslab) · [glassislands](https://ui.surf/zenit/es/components/glassislands) · [glassmotion](https://ui.surf/zenit/es/components/glassmotion) · [glasschrome](https://ui.surf/zenit/es/components/glasschrome) · [canvasevents](https://ui.surf/zenit/es/components/canvasevents) … |

[Video](https://ui.surf/zenit/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://ui.surf/zenit/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://ui.surf/zenit/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. 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://ui.surf/zenit/media/stories/modal.mp4?v=f1b080996c)

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

[Video](https://ui.surf/zenit/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://ui.surf/zenit/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.
