---
title: "Estilos y temas — Docs de zenit Zig UI"
description: "Usa tokens, funciones de estilo con nombre y recipes para que lo visual y la lógica de negocio sigan siendo independientes."
url: https://zenit.z.express/es/docs/guide/styling
language: es
alternate_en: https://zenit.z.express/docs/guide/styling.md
alternate_zh: https://zenit.z.express/zh/docs/guide/styling.md
alternate_ja: https://zenit.z.express/ja/docs/guide/styling.md
alternate_ko: https://zenit.z.express/ko/docs/guide/styling.md
alternate_fr: https://zenit.z.express/fr/docs/guide/styling.md
alternate_de: https://zenit.z.express/de/docs/guide/styling.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# Estilos y temas

Usa tokens, funciones de estilo con nombre y recipes para que lo visual y la lógica de negocio sigan siendo independientes.

## El modelo de tres capas

**01 · ThemeTokens**

La única fuente de colores, tamaños de texto, espaciado, radios y métricas de controles.

**02 · styles.zig**

Funciones puras que puedes nombrar, reutilizar, probar… y volver a ejecutar cuando cambia el tema.

**03 · Recipe**

Gestiona variantes, condiciones de interacción y componentes de varias partes.

STYLE LAYERS

Cada capa solo lee la que tiene debajo: las vistas nunca fijan un color a mano, así que cambiar de tema consiste en sustituir solo la capa inferior.

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

La matriz de Button: variant × size, icono, loading, disabled y block, con estilos resueltos por una recipe.

[Video](https://zenit.z.express/media/stories/glassbox.mp4?v=570227ca3d)

GlassBox en los modos regular, interactive y clear con un backdrop blur real.

## Tokens primero

Lee los valores de estilo de `cx.tokens` (o del `t` que recibe una función de estilo). Para valores de diseño que de verdad quedan fuera de la escala, márcalos con `ui.arb`: en tiempo de ejecución es una identidad sin coste, y su valor es semántico — quien lee distingue un valor arbitrario intencionado de un literal perezoso que debería convertirse en token.

```zig
// Preferred: follows the theme
.background = cx.tokens.color.bg_primary,
.padding = ui.Padding.all(cx.tokens.space._4),
.font_size = cx.tokens.font_size.xl,

// Legitimate escape hatch: states that the value is intentionally off-scale
.font_size = ui.arb.px(18),
.background = ui.arb.hex(0x316ff6),
.border_color = ui.arb.hexA(0x000000, 0.12),
```

> WARNING
> 
> **Evita los literales sueltos.** Los valores `Color.hex(...)` y `font_size` fijos en las vistas se saltan el tema. `scripts/check_style_literals.sh` marca los literales sueltos y deja pasar `ui.arb`, así que buscar `ui.arb` te da el inventario de todos los valores fuera de token. Cuando el mismo valor arb aparece tres o más veces, plantéate convertirlo en token.

## Funciones de estilo con nombre

Una función de estilo tiene la firma `fn (*const ui.ThemeTokens) ui.BoxStyle` (o `ui.TextStyle`). Agrúpalas en `styles.zig` y deja que las vistas las referencien por nombre.

`styles.zig`

```zig
const ui = @import("ui");

pub fn card(t: *const ui.ThemeTokens) ui.BoxStyle {
    return .{
        .background = t.color.bg_secondary,
        .padding = ui.Padding.all(t.space._6),
        .gap = t.space._3,
    };
}

pub fn row(t: *const ui.ThemeTokens) ui.BoxStyle {
    return .{ .gap = t.space._2 };
}

pub fn title(t: *const ui.ThemeTokens) ui.TextStyle {
    return .{
        .font_size = t.font_size.xxl,
        .font_weight = 600,
        .color = t.color.fg_primary,
    };
}
```

`view.zig`

```zig
const S = @import("styles.zig");

const card = try ui.boxStyled(cx, S.card, .{});
try card.appendChild(
    cx.allocator,
    try ui.textStyled(cx, S.title, "Workspace"),
);

// hstackStyled / vstackStyled force direction = .row / .column.
const actions = try ui.hstackStyled(cx, S.row, .{});
try card.appendChild(cx.allocator, actions);
```

`boxStyled`, `textStyled`, `hstackStyled` y `vstackStyled` registran en el nodo un hook `on_theme` que recuerda la propia función de estilo. Cuando cambia el tema, el nodo recalcula su estilo con los nuevos tokens; los tokens leídos dentro de un `ui.box` normal son solo una instantánea del momento del montaje.

## Cambiar de tema

Se incluyen tres temas: `ui.theme.light`, `ui.theme.dark` y `ui.theme.high_contrast`. `cx.setTheme` cambia el puntero de tokens, recorre el subárbol de `cx.root` volviendo a ejecutar los hooks `on_theme` y `before_render` de cada nodo, y los marca como sucios. Para acceder de forma reactiva al tema actual, usa `cx.themeSignal(scope)`.

`theme.zig`

```zig
// Built-in palettes: ui.theme.light, ui.theme.dark, ui.theme.high_contrast
cx.setTheme(&ui.theme.dark);

// Reactive access: a Signal that setTheme updates.
const theme = try cx.themeSignal(scope);
try scope.createEffect(.{ .theme = theme }, struct {
    fn run(ctx: anytype) void {
        const t = ctx.theme.get();
        std.log.info("theme: {s} ({s})", .{ t.name, @tagName(t.scheme) });
    }
}.run);
```

> NOTE
> 
> **La reejecución cubre el subárbol de cx.root.** Las capas flotantes de los componentes (Modal, Popover, Menu, Tooltip) viven en el `WindowOverlayPortal` bajo `cx.root`, así que `setTheme` también las vuelve a ejecutar; solo quedan fuera los árboles de nodos separados de `cx.root`. Lo que se vuelve a ejecutar son los hooks que registran los nodos styled — la mayoría de los `ui.widgets.*` toman una instantánea de los tokens al montarse y siguen necesitando una reconstrucción.

Las diferencias entre claro y oscuro deben expresarse en la paleta completa de tokens; lee `t.scheme` (`.light` / `.dark`) dentro de una función de estilo solo cuando de verdad necesites una bifurcación.

Para aplicar el color de tu marca o crear tus propios temas claro / oscuro, consulta [Temas personalizados](https://zenit.z.express/es/docs/guide/theming).

## Recipes y condiciones

`ui.recipe` es un espacio de nombres: `ui.recipe.recipe(Config)` define una recipe de un solo nodo y `ui.recipe.slotRecipe(Config)` una de varias partes. La resolución combina **base → variants → derived → compounds**, y los campos no nulos posteriores ganan; el override de estilo externo del componente se aplica al final.

RECIPE MERGE

Cada paso sobrescribe solo los campos que define. Por convención, derived solo produce geometría (padding, radio, tamaño) y nunca toca background.

| Miembro de Config | Función |
| --- | --- |
| `Variants` | Obligatorio. Cada campo es una dimensión de variante (enum o bool) con un valor por defecto |
| `base(t)` | Opcional. El ConditionalStyle base |
| `variants` | Opcional. Un resolver por dimensión: fn (value, t) ConditionalStyle |
| `derived(v, t)` | Opcional. Ve todas las Variants, para lógica continua entre dimensiones (p. ej. padding = f(size, modo icono)) |
| `compounds` | Opcional. { matches(v), style(t) } que se aplica cuando coinciden varias dimensiones a la vez |

Este es el código real de `examples/hello_button/styles.zig`: las apps también pueden definir sus propias recipes y luego fijar una variante con `resolveBase` en una función de estilo que acepta `ui.boxStyled`.

`examples/hello_button/styles.zig`

```zig
pub const PanelEmphasis = enum { neutral, highlight };

/// CVA-style: base + one variant dimension, all from tokens.
const PanelRecipe = ui.recipe.recipe(struct {
    pub const Variants = struct {
        emphasis: PanelEmphasis = .neutral,
    };

    pub fn base(t: *const ui.ThemeTokens) ui.ConditionalStyle {
        return .{ .base = .{
            .padding = ui.Padding.symmetric(t.space._2, t.space._4),
            .corner_radius = t.radius.md,
        } };
    }

    pub const variants = .{
        .emphasis = struct {
            fn resolve(e: PanelEmphasis, t: *const ui.ThemeTokens) ui.ConditionalStyle {
                return switch (e) {
                    .neutral => .{ .base = .{ .background = t.color.bg_secondary } },
                    .highlight => .{ .base = .{
                        .background = t.color.bg_tertiary,
                        .border = .{ .width = 1, .color = t.color.accent, .radius = 0 },
                    } },
                };
            }
        }.resolve,
    };
});

/// Freeze a variant into a theme-safe style function for ui.boxStyled.
pub fn panel(comptime emphasis: PanelEmphasis) fn (*const ui.ThemeTokens) ui.BoxStyle {
    return struct {
        fn f(t: *const ui.ThemeTokens) ui.BoxStyle {
            return PanelRecipe.resolveBase(.{ .emphasis = emphasis }, t);
        }
    }.f;
}
```

Además de `base`, un `ConditionalStyle` tiene siete ranuras de condición: `selected`, `expanded`, `hover`, `active`, `focus`, `invalid`, `disabled`. `resolve(state)` superpone selected → expanded → hover → active → focus → invalid en ese orden; `disabled` cortocircuita, así que nada más se apila sobre un nodo deshabilitado.

`resolve.zig`

```zig
// Full ConditionalStyle for one variant combination.
const cs = PanelRecipe.resolve(.{ .emphasis = .highlight }, cx.tokens);

// A component's style props are applied last, on top of the recipe output.
const merged = cs.override(.{
    .style = .{ .background = cx.tokens.color.accent },
});

// Pick the final style for the current interaction state.
const final: ui.BoxStyle = merged.resolve(.{ .is_hovered = true });
```

| Situación | Herramienta |
| --- | --- |
| Un estilo estático | Función de estilo con nombre |
| Variantes de tamaño / visuales | `ui.recipe.recipe` |
| Componentes de varias partes (inputs, menús) | `ui.recipe.slotRecipe` |
| hover / disabled / invalid | `ui.ConditionalStyle` |
| Claro / oscuro | Una paleta completa de tokens; lee `t.scheme` si no queda otra |

## Límites actuales

-   **Los componentes leen los tokens sobre todo al montarse.** Las partes de `ui.widgets.*` que no registran un hook para volver a leer los tokens conservan la instantánea del montaje. Tras un cambio de tema en tiempo de ejecución, los nodos styled de la app se vuelven a ejecutar, pero los nodos de la biblioteca de componentes pueden requerir reconstruir el árbol.
    
-   **La reejecución cubre el subárbol de cx.root.** El portal de capas flotantes de la ventana está dentro; los árboles separados de `cx.root` no se actualizan.
    
-   **themeSignal sigue al Scope que lo creó.** Créalo en el Scope raíz de la app para que sobreviva a todos sus suscriptores.
    

Consulta el [sitio de componentes](https://zenit.z.express/es/components) para ver la matriz de variantes y tamaños de cada componente.
