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
La única fuente de colores, tamaños de texto, espaciado, radios y métricas de controles.
Funciones puras que puedes nombrar, reutilizar, probar… y volver a ejecutar cuando cambia el tema.
Gestiona variantes, condiciones de interacción y componentes de varias partes.
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.
// 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.
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,
};
}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).
// 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.
VariantsObligatorio. Cada campo es una dimensión de variante (enum o bool) con un valor por defectobase(t)Opcional. El ConditionalStyle basevariantsOpcional. Un resolver por dimensión: fn (value, t) ConditionalStylederived(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 vezEste 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.
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.
// 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 });ui.recipe.recipeui.recipe.slotRecipeui.ConditionalStylet.scheme si no queda otraLí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.rootno 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.