---
title: "Temas personalizados — Docs de zenit Zig UI"
description: "Un tema es un único valor ThemeTokens: colores, espaciado, radios, tamaños de texto y métricas de controles."
url: https://zenit.z.express/es/docs/guide/theming
language: es
alternate_en: https://zenit.z.express/docs/guide/theming.md
alternate_zh: https://zenit.z.express/zh/docs/guide/theming.md
alternate_ja: https://zenit.z.express/ja/docs/guide/theming.md
alternate_ko: https://zenit.z.express/ko/docs/guide/theming.md
alternate_fr: https://zenit.z.express/fr/docs/guide/theming.md
alternate_de: https://zenit.z.express/de/docs/guide/theming.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# Temas personalizados

Un tema es un único valor ThemeTokens: colores, espaciado, radios, tamaños de texto y métricas de controles. Copia un preset incluido, cambia los campos que llevan tu marca, instálalo con cx.setTheme y ya es tu tema.

Esta página trata de definir e instalar un tema. Cómo consumen los tokens las vistas — funciones de estilo y recipes — se explica en [Estilos y temas](https://zenit.z.express/es/docs/guide/styling).

## El tema por defecto

El `Cx` de cada ventana empieza en `ui.theme.dark`. Una app que nunca llama a `setTheme` es oscura — Hello Button lo es. Para usar otro tema, establécelo en la primera línea de tu función de montaje, antes de que exista ningún nodo:

`main.zig`

```zig
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;
}
```

Con varias ventanas, cada una tiene su propio `Cx`, así que establece el tema en cada función de montaje. zenit **no** sigue todavía la apariencia del sistema: un cambio claro/oscuro del sistema solo provoca un redibujado, y no hay una API pública para leer la apariencia del sistema. Ofrece en su lugar un interruptor a tus usuarios (consulta “Cambiar en tiempo de ejecución”).

## Anatomía de ThemeTokens

`src/ui/theme.zig (extracto)`

```zig
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
};
```

Todo salvo `color` tiene valores por defecto. Los 54 colores de `ColorTokens` no tienen ninguno, así que no escribas un literal de struct desde cero: copia `ui.theme.light`, `ui.theme.dark` o `ui.theme.high_contrast` y cambia solo lo que te interese.

> NOTE
> 
> **Usa scheme para distinguir claro de oscuro.** `scheme` es el único indicador claro/oscuro: el Notifier elige su paleta a partir de él y DevTools deriva sus colores de él. Nunca busques “dark” en `name`: el tema de alto contraste es negro puro y su nombre no dice nada parecido.

## Derivar un tema de marca

Escribe los temas como `const` a nivel de contenedor; el bloque inicializador se ejecuta en tiempo de compilación y el resultado tiene almacenamiento estático, de modo que el puntero que guarda `cx.setTheme(&brand)` sigue siendo válido durante toda la vida del programa. Nunca tomes la dirección de un tema guardado en una variable local de una función.

`theme.zig`

```zig
// 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;
};
```

> WARNING
> 
> **Los tokens de componentes no siguen a accent.** En los presets, `accent` y `button_primary_bg` son valores independientes: no se deriva nada. Cuando cambies el color de marca, cambia a la vez `accent`, `button_primary_bg` / `button_primary_fg` y `border_focus`, o los botones primary conservarán el color del preset.

## Un tema oscuro a juego

Deriva la variante oscura de `ui.theme.dark` en lugar de recolorear el tema de marca claro: las decenas de valores de fondo, texto y borde del preset oscuro están ajustados como conjunto. Sobre fondos oscuros, el color de marca suele necesitar aclararse, y el texto de los botones primary pasa a ser oscuro.

`theme.zig`

```zig
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 (claro)](https://zenit.z.express/media/theme-brand.webp?v=588f003545)brand (claro)

![brand_dark](https://zenit.z.express/media/theme-brand-dark.webp?v=854df7b29f)brand\_dark

![ui.theme.dark (por defecto)](https://zenit.z.express/media/theme-default-dark.webp?v=3b5b953dc1)ui.theme.dark (por defecto)

El mismo Hello Button cambiando solo el argumento de setTheme — las tres capturas las tomó el harness E2E en la ventana real.

## Qué tokens afectan a qué

Agrupados según lo que realmente leen los componentes (código fuente de v0.1.0-alpha). Empieza por estos al crear un tema:

| Token | Uso principal |
| --- | --- |
| `fg_primary · fg_secondary · fg_tertiary · fg_disabled` | Casi todo el texto y los iconos |
| `bg_primary · bg_secondary · bg_tertiary` | Paneles, tarjetas, Calendar, Chip, pistas de Progress / Slider, Skeleton |
| `bg_hover · bg_active` | Estados hover y pulsado de los controles clicables |
| `accent` | Checkbox / Switch marcados, enlaces, Tabs, Breadcrumb, fechas seleccionadas, Badge |
| `button_primary_bg · button_primary_fg` | Button primary (hover / pulsado se derivan del fondo); fg también invierte el texto en Badge, Chip y Tabs |
| `border · border_strong · separator` | Bordes y separadores |
| `border_focus · input_bg · input_border · selection_bg` | Input, Select, NumberStepper, DatePicker y otros campos |
| `success · warning · danger · info · status_fg` | Alert, Badge, Tag, Progress, Button danger |
| `list_hover_bg · list_selection_bg` | Menu, DropdownMenu, ComboBox, Table, Tree, DataTable |
| `checkbox_* · switch_thumb · switch_track_off` | Checkbox / Switch sin marcar y deshabilitados |
| `tooltip_bg · tooltip_fg · overlay · scrollbar_thumb` | Tooltip, velo de Modal / Sheet, barras de desplazamiento |

> WARNING
> 
> **Ningún componente lee todavía estos tokens.** `accent_muted`, `bg_inset`, `button_secondary_bg`, `button_secondary_fg`, `switch_track_on` (un Switch activado usa `accent`), `table_header_bg`, `list_selection_hover_bg`, y `success_subtle` / `warning_subtle` / `danger_subtle` / `info_subtle`. Cambiarlos hoy no tiene efecto visible; tus propias funciones de estilo pueden seguir usándolos.

Algunos componentes llevan sus propios colores en lugar de leer `ColorTokens`: el Notifier elige entre sus paletas clara y oscura incluidas según `scheme`, y los tintes de GlassBox y algunos detalles de Progress y Select son valores fijos. Siguen el modo claro/oscuro, pero no tu color de marca.

## Radios, espaciado y métricas de controles

Los tokens que no son de color pueden sobrescribirse campo a campo. El más útil es `control`: Button, Input, Select, ComboBox, DatePicker, DateRangePicker, Tabs y Chip comparten cuatro conjuntos de métricas, xs / sm / md / lg. La altura de un control no es un token: se calcula:

| Tamaño | Altura por defecto = padding\_y × 2 + font\_size × line\_height |
| --- | --- |
| `xs` | 2.5 × 2 + 12 × 1.25 = 20 |
| `sm` | 4.5 × 2 + 12 × 1.25 = 24 |
| `md` | 7.25 × 2 + 14 × 1.25 = 32 |
| `lg` | 10 × 2 + 16 × 1.25 = 40 |

Así que, para que los controles md sean más altos, cambia `t.control.md.padding_y` o el tamaño de fuente y todos los controles md lo seguirán; no fijes a mano la `height` de un control. Mantén `icon_size` no mayor que la altura de línea.

## Cambiar en tiempo de ejecución

`cx.setTheme` puede llamarse en cualquier momento: cambia el puntero de tokens, vuelve a ejecutar los hooks `on_theme` / `before_render` en todo el subárbol de `cx.root` (incluido el portal de capas flotantes) y solicita un redibujado.

`prefs.zig`

```zig
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),
```

```zig
// 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;
```

> NOTE
> 
> **Hasta dónde llega un cambio en tiempo de ejecución.** Los nodos creados con `boxStyled` / `textStyled` y los componentes que registran hooks de tema cambian de color al instante; la mayoría de los `ui.widgets.*` leen los tokens al montarse, así que reconstruye esos subárboles para un cambio completo. Las apps que eligen su tema una sola vez al arrancar no se ven afectadas.

## Antes de publicar

-   `scheme` coincide con el fondo (un fondo oscuro debe ser `.dark`).
    
-   `button_primary_fg` sobre `button_primary_bg` y `status_fg` sobre `danger` alcanzan al menos 4.5:1 — la misma comprobación con la que se prueban los presets incluidos.
    
-   `border` sigue distinguiéndose de `bg_primary` (los tests de los presets exigen ≥ 1.2).
    
-   Cambia el Storybook a tu tema y recorre los componentes habituales (la story Component Wall es un buen punto de partida).
