# Theme cookbook

> Complete, runnable Pyzed themes to copy: a brand theme in light and dark, high contrast, retro terminal, glassy dark, playful pastel and two themes in one window.

Source: https://albakore.github.io/pyzed/theming/cookbook/

Six complete themes, ready to copy into your app. Each one is a file in
[`examples/theming/`](https://github.com/albakore/pyzed/tree/main/examples/theming) and draws the same screen
(`preview.py`: buttons in every variant, a small form, badges, a callout and two stats), so you can compare how each
theme changes it.

## Run them

From a clone of the repository (see [Running the examples](https://albakore.github.io/pyzed/examples/)):

**uv:**

```bash
uv run python examples/theming/brand.py
```

**pip:**

```bash title="macOS and Linux"
python3 examples/theming/brand.py
```

```powershell title="Windows (PowerShell)"
py examples\theming\brand.py
```

Replace `brand.py` with `high_contrast.py`, `retro_terminal.py`, `glassy_dark.py`, `pastel.py` or `mixed.py`.

**Copying one into your project:**

Copy the theme part (everything above `class ...App`) into a module of your project, such as `mytheme.py`, and import
it before the app starts. The `preview` import is only for these examples.

## Brand theme, light and dark

A company palette in two versions that follow the system's light or dark mode. `Theme.generate` builds each version
from three colors; `derive` sets the brand's own green for success.

`examples/theming/brand.py`:

```py
"""A brand theme in light and dark that follows the operating system.

    python examples/theming/brand.py

- ``Theme.generate`` builds a whole theme from a background, a text and an accent color.
- ``derive`` changes a few tokens on top (rounder corners, the brand's own green for success).
- ``themes.register`` gives the themes a name, and ``themes.auto`` picks light or dark like the OS.
"""

import pyzed as pz
from preview import preview

BRAND = "#6d28d9"  # the brand's purple

ACME_LIGHT = pz.Theme.generate(
    "Acme Light", background="#ffffff", text="#1f1b2e", accent=BRAND, radius=10,
).derive(success="#15803d")

ACME_DARK = pz.Theme.generate(
    "Acme Dark", background="#16131f", text="#ece9f5", accent="#a78bfa", radius=10,
).derive(success="#4ade80")

pz.themes.register(ACME_LIGHT)
pz.themes.register(ACME_DARK)

class BrandApp(pz.App):
    title = "Acme"
    size = (720, 580)
    theme = pz.themes.auto("Acme Light", "Acme Dark")  # light or dark, like the system

    def view(self):
        return pz.container(preview("Acme", "A brand theme that follows the system's light or dark mode."),
                            padding=24, width="fill", height="fill")

if __name__ == "__main__":
    pz.run(BrandApp)
```

**What to change:** the three colors of each `generate`, and `radius` for rounder or squarer corners.

## High contrast

Pure black and white with yellow focus, for people who need strong contrast (and a good test of how your app looks
with large, clear borders). `Theme.from_dict` lists every token that changes; the rest come from One Dark. The
script prints the contrast ratio of the main colors when it starts.

`examples/theming/high_contrast.py`:

```py
"""A high-contrast theme for readers who need strong colors and clear edges.

    python examples/theming/high_contrast.py

- ``Theme.from_dict`` starts from One Dark and replaces only the tokens you list.
- ``pz.theme.contrast`` checks the WCAG contrast ratio of two colors (7:1 or more is "AAA").
- A global ``style`` makes every text bigger.
"""

import pyzed as pz
from preview import preview

HIGH_CONTRAST = pz.Theme.from_dict({
    "name": "High Contrast",
    "appearance": "dark",
    # surfaces: pure black, panels a touch lighter so they stay visible
    "background": "#000000", "surface": "#0a0a0a", "elevated": "#141414", "editor": "#000000",
    "panel": "#0a0a0a", "title_bar": "#000000", "status_bar": "#000000",
    # strong white borders
    "border": "#ffffff", "border_variant": "#bdbdbd",
    # text: white, and muted text that is still very readable
    "text": "#ffffff", "text_muted": "#e0e0e0", "text_placeholder": "#c2c2c2", "text_disabled": "#9e9e9e",
    "icon": "#ffffff", "icon_muted": "#e0e0e0",
    # buttons and list rows
    "element": "#1f1f1f", "element_hover": "#333333", "element_active": "#4d4d4d",
    "ghost_hover": "#262626", "ghost_active": "#3d3d3d", "ghost_selected": "#3d3d3d",
    # status colors that stay readable on black
    "error": "#ff6b6b", "warning": "#ffd600", "success": "#5cff8a", "info": "#6bc8ff",
    # square corners
    "radius": 0, "radius_sm": 0,
}).with_accent("#ffd600")  # yellow focus rings, selection and primary buttons

pz.themes.register(HIGH_CONTRAST)

def report(theme):
    """Prints the contrast ratio of the main text colors against the background."""
    for token in ("text", "text_muted", "accent", "error"):
        ratio = pz.theme.contrast(theme.color(token), theme.background)
        print(f"{token:>10} on background: {ratio:4.1f}:1")

class HighContrastApp(pz.App):
    title = "High contrast"
    size = (720, 580)
    theme = "High Contrast"
    style = pz.Style(size=15)  # every text that does not set its own size

    def view(self):
        return pz.container(preview("High contrast", "Black, white and yellow, with square corners."),
                            padding=24, width="fill", height="fill")

if __name__ == "__main__":
    report(HIGH_CONTRAST)
    pz.run(HighContrastApp)
```

**Accessibility:**

A theme is one part of accessibility. See [Accessibility](https://albakore.github.io/pyzed/guides/accessibility/) for keyboard navigation,
screen readers and reduced motion.

## Retro terminal

Green phosphor on black. One green becomes the text, the accent and the success color; the global style makes every
text monospaced.

`examples/theming/retro_terminal.py`:

```py
"""A retro terminal theme: green phosphor on black, monospaced and square.

    python examples/theming/retro_terminal.py

- ``Theme.generate`` with a green text color and a green accent.
- ``syntax`` and ``ansi`` recolor code and the terminal to match.
- The global style switches every text to the monospaced font.
"""

import pyzed as pz
from preview import preview

PHOSPHOR = "#33ff66"

RETRO = pz.Theme.generate(
    "Retro Terminal",
    background="#050a05",
    text=PHOSPHOR,
    accent=PHOSPHOR,
    error="#ff5555",
    warning="#ffcc33",
    success=PHOSPHOR,
    radius=0,
    syntax={"keyword": ("#a6ff00", "bold"), "string": "#7dffb0", "comment": ("#1f7a3a", "italic")},
).derive(ansi={"green": PHOSPHOR, "bright_black": "#1f7a3a"})

pz.themes.register(RETRO)

class RetroApp(pz.App):
    title = "Retro terminal"
    size = (720, 640)
    theme = "Retro Terminal"
    style = pz.Style(font="mono", size=13)  # every text in the window is monospaced

    def view(self):
        title = pz.text("> READY_").style(size=26, weight="bold", color="accent")
        return pz.container(pz.column([title, preview("SYSTEM", "All colors come from one green.")], spacing=12),
                            padding=24, width="fill", height="fill")

if __name__ == "__main__":
    pz.run(RetroApp)
```

## Glassy dark

A gradient behind the window and a frosted glass panel on top. The theme is One Dark with a cyan accent; the look
comes from the global style (`background=` a gradient) and the `glass` effect.

`examples/theming/glassy_dark.py`:

```py
"""A glassy dark look: a gradient behind the window and a frosted panel on top.

    python examples/theming/glassy_dark.py

- The global style paints the window with a ``pz.Gradient``.
- ``glass`` turns a container into a translucent panel that blurs what is behind it.
- The theme itself is One Dark with a cyan accent (``with_accent``) and rounder corners.
"""

import pyzed as pz
from preview import preview

GLASSY = pz.themes.ONE_DARK.derive(name="Glassy Dark", radius=14, radius_sm=10).with_accent("#22d3ee")
pz.themes.register(GLASSY)

NIGHT = pz.Gradient(["#0f172a", "#312e81", "#701a75"], angle=135)

class GlassyApp(pz.App):
    title = "Glassy dark"
    size = (760, 650)
    theme = "Glassy Dark"
    style = pz.Style(background=NIGHT)  # paints the whole window

    def view(self):
        panel = pz.container(preview("Glassy", "A frosted panel over a gradient."), width="fill").style(
            glass={"tint": "#ffffff", "strength": 0.08, "blur": 24}, radius=20, padding=24,
            border=(1, "#ffffff22"))
        return pz.container(panel, padding=28, width="fill", height="fill")

if __name__ == "__main__":
    pz.run(GlassyApp)
```

**Effects and the GPU:**

`glass`, `backdrop_blur` and `glow` are made for the GPU renderer. On machines without a GPU (some virtual machines
and remote desktops) or with the CPU renderer (`ICED_BACKEND=tiny-skia`), they can look simpler than in this
capture. Make sure the text stays readable without them.

## Playful pastel

A cream background, candy colors and very round corners. `derive` sets pastel avatar colors (`players`), and a
named style (`pz.define_style("bubble", ...)`) draws the card around the screen.

`examples/theming/pastel.py`:

```py
"""A playful pastel theme: cream background, candy colors and very round corners.

    python examples/theming/pastel.py

- ``Theme.generate`` for a light theme, then ``derive`` with a pastel info color and avatar colors.
- ``pz.define_style`` names a reusable "bubble" style for the card around everything.
"""

import pyzed as pz
from pyzed import ui
from preview import preview

PASTEL = pz.Theme.generate(
    "Pastel", background="#fffaf3", text="#4a3f55", accent="#f472b6", radius=16,
    success="#34d399", warning="#f59e0b", error="#fb7185",
).derive(info="#60a5fa", players=("#f472b6", "#a78bfa", "#60a5fa", "#34d399", "#fbbf24"))
pz.themes.register(PASTEL)

pz.define_style("bubble", background="surface", radius=24, padding=20, border=(2, "#f9a8d4"),
                shadow=("#f472b633", (0, 8), 24))

class PastelApp(pz.App):
    title = "Pastel"
    size = (720, 670)
    theme = "Pastel"

    def view(self):
        friends = pz.row([ui.avatar(name, index=i, size=30) for i, name in enumerate(["AL", "BO", "CY", "DI", "EV"])],
                         spacing=6)
        return pz.container(pz.column([
            pz.container(preview("Hello!", "Soft colors and round corners."), width="fill").style("bubble"),
            friends,
        ], spacing=16), padding=24, width="fill", height="fill")

if __name__ == "__main__":
    pz.run(PastelApp)
```

## Two themes in one window

A light sidebar in a dark app, with a theme picker that saves the chosen theme to a JSON file and loads it on the
next start (`Theme.save` and `Theme.load`).

with theme:</code> draws the sidebar with One Light." />

`examples/theming/mixed.py`:

```py
"""Two themes in one window, and a theme picker that remembers your choice.

    python examples/theming/mixed.py

- ``with theme:`` draws a block with another theme (here, a light sidebar in a dark app).
- ``self.theme = name`` changes the theme at runtime; the whole window restyles at once.
- ``Theme.save`` and ``Theme.load`` keep the chosen theme in a JSON file.
"""

from pathlib import Path

import pyzed as pz
from pyzed import ui
from preview import preview

SAVED = Path(__file__).with_name("my-theme.json")

class MixedApp(pz.App):
    title = "Mixed themes"
    size = (980, 580)

    def __init__(self):
        self.theme = "One Dark"
        if SAVED.exists():
            self.theme = pz.themes.register(pz.Theme.load(SAVED)).name

    def pick(self, name):
        self.theme = name
        pz.themes.get(name).save(SAVED)  # remembered for the next start

    def view(self):
        current = self.resolved_theme().name
        with pz.themes.get("One Light"):  # everything built in this block uses One Light
            sidebar = pz.container(pz.column([
                ui.heading("Settings", level=3),
                ui.field("Theme", ui.select(pz.themes.names(), current, self.pick, width=200)),
                ui.muted("This panel always uses One Light."),
            ], spacing=12), padding=16, width=300, height="fill").style(background="background")
        main = pz.container(preview(current, "The main area follows the theme you pick."),
                            padding=24, width="fill", height="fill")
        return pz.row([sidebar, main], height="fill")

if __name__ == "__main__":
    pz.run(MixedApp)
```

## The shared preview

`examples/theming/preview.py`:

```py
"""A small "kitchen sink" used by every example in this folder.

It only uses theme tokens and ``pyzed.ui`` components, so it takes the look of whatever
theme is current: run any of the examples next to it to see the same screen restyled.

    python examples/theming/brand.py
"""

import pyzed as pz
from pyzed import ui

def preview(title: str = "Preview", subtitle: str = "Every color on this screen comes from the theme."):
    """A heading, buttons in every variant, a small form, badges, a callout and two stats."""
    buttons = pz.row([
        ui.button("Primary", lambda: None, variant="solid"),
        ui.button("Filled", lambda: None),
        ui.button("Outline", lambda: None, variant="outline"),
        ui.button("Ghost", lambda: None, variant="ghost"),
        ui.button("Delete", lambda: None, variant="tinted", tint="error", icon="trash-2"),
    ], spacing=8, wrap=True)
    form = pz.column([
        ui.field("Name", ui.input("Ada Lovelace", lambda _v: None, width=220)),
        ui.field("Email", ui.input("", lambda _v: None, placeholder="you@example.com", width=220)),
        ui.field("Notifications", ui.switch(True, lambda _v: None)),
        ui.field("Volume", pz.container(ui.slider(0.6, lambda _v: None), width=220)),
    ], spacing=10)
    badges = pz.row([
        ui.badge("New"),
        ui.badge("Paid", tint="success"),
        ui.badge("Pending", tint="warning"),
        ui.badge("Failed", tint="error", variant="outline"),
        ui.badge("Beta", tint="info", variant="solid"),
    ], spacing=6, wrap=True)
    stats = pz.row([
        ui.stat("Revenue", "$12.4k", delta=8.2, width="fill"),
        ui.stat("Refunds", "31", delta=-2.5, width="fill"),
    ], spacing=12)
    return pz.column([
        pz.column([ui.heading(title), ui.muted(subtitle)], spacing=4),
        buttons,
        ui.card(pz.column([form, ui.progress(0.7)], spacing=14), title="Account"),
        badges,
        ui.callout("Saved", "Your changes are stored on this device.", tint="success"),
        stats,
    ], spacing=16)
```

Every color in it is a token or a tint, which is why one file looks right in six very different themes. Write your
own views the same way.
