docs/guide/styling
Conceptos clave · Design system

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.

12 min de lectura

El modelo de tres capas

01
01 · ThemeTokens

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

02
02 · styles.zig

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

03
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.
La matriz de Button: variant × size, icono, loading, disabled y block, con estilos resueltos por una recipe.
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),

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

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.

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 ConfigFunción
VariantsObligatorio. Cada campo es una dimensión de variante (enum o bool) con un valor por defecto
base(t)Opcional. El ConditionalStyle base
variantsOpcional. 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))
compoundsOpcional. { 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.

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
// 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ónHerramienta
Un estilo estáticoFunción de estilo con nombre
Variantes de tamaño / visualesui.recipe.recipe
Componentes de varias partes (inputs, menús)ui.recipe.slotRecipe
hover / disabled / invalidui.ConditionalStyle
Claro / oscuroUna 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 para ver la matriz de variantes y tamaños de cada componente.

zenit · Doble licenciaGratis para proyectos de código abierto bajo GPL-3.0-only; los productos cerrados o comerciales necesitan una licencia comercial.Contacta con el autor: zongyi.xzy#gmail.com (cambia # por @)zenit 5f9add5+wip 2026-09-30