---
title: "Styling & Themes — zenit Zig UI Doku"
description: "Mit Tokens, benannten Stilfunktionen und Recipes bleiben Optik und Geschäftslogik voneinander unabhängig."
url: https://zenit.z.express/de/docs/guide/styling
language: de
alternate_en: https://zenit.z.express/docs/guide/styling.md
alternate_zh: https://zenit.z.express/zh/docs/guide/styling.md
alternate_es: https://zenit.z.express/es/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
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# Styling und Themes

Mit Tokens, benannten Stilfunktionen und Recipes bleiben Optik und Geschäftslogik voneinander unabhängig.

## Das Drei-Schichten-Modell

**01 · ThemeTokens**

Die einzige Quelle für Farben, Schriftgrößen, Abstände, Radien und Control-Maße.

**02 · styles.zig**

Reine Funktionen, die Sie benennen, wiederverwenden, testen — und beim Theme-Wechsel erneut ausführen können.

**03 · Recipe**

Behandelt Varianten, Interaktionszustände und mehrteilige Komponenten.

STYLE LAYERS

Jede Schicht liest nur die darunterliegende: Views codieren nie eine Farbe fest, ein Theme-Wechsel tauscht also nur die unterste Schicht aus.

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

Die Button-Matrix aus variant × size, Icon, loading, disabled und block, gestylt über eine Recipe.

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

GlassBox in den Modi regular, interactive und clear mit echtem Backdrop-Blur.

## Tokens zuerst

Lesen Sie Stilwerte aus `cx.tokens` (oder dem `t`, das eine Stilfunktion erhält). Designwerte, die wirklich außerhalb der Skala liegen, markieren Sie mit `ui.arb`: Zur Laufzeit ist das eine kostenlose Identitätsfunktion, ihr Wert ist semantisch — Lesende unterscheiden einen bewusst gewählten Sonderwert von einem bequemen Literal, das ein Token werden sollte.

```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
> 
> **Vermeiden Sie nackte Literale.** `Color.hex(...)` und feste `font_size`\-Werte in Views umgehen das Theme. `scripts/check_style_literals.sh` meldet nackte Literale und lässt `ui.arb` durch, sodass ein grep nach `ui.arb` alle Werte außerhalb der Tokens auflistet. Taucht derselbe arb-Wert dreimal oder öfter auf, sollten Sie ihn zu einem Token befördern.

## Benannte Stilfunktionen

Eine Stilfunktion hat die Signatur `fn (*const ui.ThemeTokens) ui.BoxStyle` (oder `ui.TextStyle`). Sammeln Sie sie in `styles.zig` und lassen Sie Views sie über ihren Namen referenzieren.

`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` und `vstackStyled` registrieren am Node einen `on_theme`\-Hook, der sich die Stilfunktion selbst merkt. Wechselt das Theme, berechnet der Node seinen Stil aus den neuen Tokens neu; in einem einfachen `ui.box` gelesene Tokens sind nur ein Snapshot vom Zeitpunkt des Mountens.

## Themes wechseln

Drei Themes sind eingebaut: `ui.theme.light`, `ui.theme.dark` und `ui.theme.high_contrast`. `cx.setTheme` tauscht den Token-Zeiger aus, durchläuft den `cx.root`\-Teilbaum, spielt dabei die `on_theme`\- und `before_render`\-Hooks jedes Nodes erneut ab und markiert sie als dirty. Für reaktiven Zugriff auf das aktuelle Theme verwenden Sie `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
> 
> **Das erneute Abspielen umfasst den cx.root-Teilbaum.** Komponenten-Overlays (Modal, Popover, Menu, Tooltip) liegen im `WindowOverlayPortal` unter `cx.root`, daher spielt `setTheme` auch sie erneut ab; nur von `cx.root` getrennte Node-Bäume bleiben außen vor. Abgespielt werden die Hooks, die styled Nodes registrieren — die meisten `ui.widgets.*` erfassen Tokens beim Mounten als Snapshot und müssen weiterhin neu aufgebaut werden.

Hell/Dunkel-Unterschiede gehören in die vollständige Token-Palette; lesen Sie `t.scheme` (`.light` / `.dark`) in einer Stilfunktion nur, wenn Sie wirklich verzweigen müssen.

Wie Sie Ihre Markenfarbe anwenden oder eigene helle / dunkle Themes bauen, lesen Sie unter [Eigene Themes](https://zenit.z.express/de/docs/guide/theming).

## Recipes und Bedingungen

`ui.recipe` ist ein Namespace: `ui.recipe.recipe(Config)` definiert eine Recipe für einen einzelnen Node, `ui.recipe.slotRecipe(Config)` eine mehrteilige. Die Auflösung führt **base → variants → derived → compounds** zusammen, spätere Nicht-null-Felder gewinnen; der externe Style-Override einer Komponente wird zuletzt angewendet.

RECIPE MERGE

Jeder Schritt überschreibt nur die Felder, die er setzt. Per Konvention erzeugt derived nur Geometrie (Padding, Radius, Größe) und rührt background nie an.

| Config-Member | Rolle |
| --- | --- |
| `Variants` | Pflicht. Jedes Feld ist eine Variantendimension (enum oder bool) mit Standardwert |
| `base(t)` | Optional. Der Basis-ConditionalStyle |
| `variants` | Optional. Ein Resolver pro Dimension: fn (value, t) ConditionalStyle |
| `derived(v, t)` | Optional. Sieht alle Variants, für dimensionsübergreifende, stetige Logik (z. B. padding = f(size, Icon-Modus)) |
| `compounds` | Optional. { matches(v), style(t) }, angewendet, wenn mehrere Dimensionen gleichzeitig zutreffen |

Hier der echte Code aus `examples/hello_button/styles.zig`: Auch Apps können eigene Recipes definieren und eine Variante dann mit `resolveBase` zu einer Stilfunktion einfrieren, die `ui.boxStyled` akzeptiert.

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

Neben `base` hat ein `ConditionalStyle` sieben Bedingungs-Slots: `selected`, `expanded`, `hover`, `active`, `focus`, `invalid`, `disabled`. `resolve(state)` schichtet selected → expanded → hover → active → focus → invalid in dieser Reihenfolge; `disabled` schließt kurz, auf einem deaktivierten Node stapelt sich also nichts weiter.

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

| Situation | Werkzeug |
| --- | --- |
| Ein statischer Stil | Benannte Stilfunktion |
| Größen- / visuelle Varianten | `ui.recipe.recipe` |
| Mehrteilige Komponenten (Eingabefelder, Menüs) | `ui.recipe.slotRecipe` |
| hover / disabled / invalid | `ui.ConditionalStyle` |
| Hell / dunkel | Eine vollständige Token-Palette; notfalls `t.scheme` lesen |

## Aktuelle Grenzen

-   **Komponenten lesen Tokens meist beim Mounten.** Teile von `ui.widgets.*`, die keinen Hook zum erneuten Lesen der Tokens registrieren, behalten ihren Snapshot vom Mounten. Nach einem Theme-Wechsel zur Laufzeit werden styled Nodes der App erneut abgespielt, Nodes der Komponentenbibliothek erfordern aber eventuell einen Neuaufbau des Baums.
    
-   **Das erneute Abspielen umfasst den cx.root-Teilbaum.** Das Overlay-Portal des Fensters liegt darin; von `cx.root` getrennte Bäume werden nicht aktualisiert.
    
-   **themeSignal folgt dem Scope, der es erzeugt hat.** Erzeugen Sie es im Root-Scope der App, damit es alle Abonnenten überlebt.
    

Die Varianten- und Größenmatrix jeder Komponente finden Sie auf der [Komponenten-Website](https://zenit.z.express/de/components).
