Skip to content

Theme Components

Colours in Phui come from the active theme's palette rather than being hardcoded, so a whole app stays consistent and can be restyled from one place.

Overview

A Palette names eight semantic roles by default — accent, text, muted, surface, success, warning, danger and info, which you can extend — and any component, built-in or your own, can draw its colours from them rather than from literal hex values. A Theme wraps a palette together with any per-component overrides; you set it once on the Screen and can swap it at runtime to restyle the whole app.

Applying a theme

A theme is a single object — a palette plus any overrides — that you hand the Screen to colour the whole app.

Set the theme

Set the theme on the Screen. A handful of palettes ship built in:

php
use Phui\Style\Palette;
use Phui\Style\Theme;

Screen::mount(App::class)
    ->theme(Theme::make(Palette::nord()))
    ->run();

Palette::default(), dracula(), nord(), gruvboxDark(), solarizedDark(), catppuccinMocha(), everforestDark() and tokyoNight() are all available.

Using a theme

To colour your own components, read the active palette from $this->themes and apply it directly — no other machinery required:

php
public function build(): Node
{
    $palette = $this->themes->palette();

    return Container::make([
        Text::make('●')->colour($palette->accent),
        Text::make(' Done')->colour($palette->text),
    ]);
}

Its colours follow the palette, so it re-themes for free, and when you want it to look different you change the code. That is all most components ever need.

Extend the palette

The base roles are a shared vocabulary, not a limit. Extend Palette with your own semantic slots, wrapping a base palette for the standard eight:

php
use Phui\Colours\Colour;

final readonly class AppPalette extends Palette
{
    public function __construct(
        public Colour $highlight,
        Palette $base,
    ) {
        parent::__construct(
            $base->accent, $base->text, $base->muted, $base->surface,
            $base->success, $base->warning, $base->danger, $base->info,
        );
    }
}

Build a theme on an instance of it and hand that to the screen, exactly as with any palette:

php
$palette = new AppPalette(
    highlight: Hex::from('#a3be8c'),
    base: Palette::nord(),
);

Screen::mount(App::class)
    ->theme(Theme::make($palette))
    ->run();

A component then reads the extra role by passing the palette class to $this->themes->palette(), which returns it typed as that class:

php
$dot = Text::make('●')->colour(
    $this->themes->palette(AppPalette::class)->highlight,
);

If the active theme's palette is not an AppPalette, that throws — so set a theme built on it first.

Switch at runtime

Swap the active theme from a handler and the whole tree rebuilds in the new colours on the next frame, through $this->themes:

php
Binding::keypress(
    Keypress::t,
    fn () => $this->themes->swap(Theme::make(Palette::dracula())),
    description: 'Switch theme',
);

A Theme is a complete, self-contained description — palette plus any overrides — so a theme you keep a reference to restores exactly as it was. Hold several and step through them:

php
/** @var Theme[] */
private array $options;

private int $active = 0;

public function onMount(): void
{
    $this->options = [
        Theme::make(Palette::nord()),
        Theme::make(Palette::dracula()),
        Theme::make(Palette::gruvboxDark()),
    ];
}

private function cycle(): void
{
    $this->active = ($this->active + 1) % count($this->options);
    $this->themes->swap($this->options[$this->active]);
}

Component colours

Both your own components and the built-ins take their colours from a *Colours object: give one to your component to make it restylable, or override a built-in's to recolour it.

Whatever the component, its colours resolve in one fixed order — a per-instance override, then a theme-wide override, then the palette-derived default — and the first that exists wins. Everything below is just a way to set one of those three layers.

Make a component re-themeable

Reach for a colours class only when other code should restyle your component without editing it — one you publish or share between apps. Within a single app the palette approach above is simpler.

A colours class is a readonly value object of Colours named after the component's parts, with the Recolourable trait for with() and a static for(Palette) factory that derives them from the active palette:

php
use Phui\Colours\Colour;
use Phui\Style\Recolourable;

final readonly class StatusBadgeColours
{
    use Recolourable;

    public function __construct(
        public Colour $label,
        public Colour $dot,
    ) {}

    public static function for(Palette $palette): self
    {
        return new self(
            label: $palette->text,
            dot: $palette->accent,
        );
    }
}

In build(), read the resolved colours with $this->colours() instead of the palette:

php
public function build(): Node
{
    $colours = $this->colours(StatusBadgeColours::class);

    return Container::make([
        Text::make('●')->colour($colours->dot),
        Text::make(' Done')->colour($colours->label),
    ]);
}

$this->colours() resolves those three layers for you, so the component re-themes with the palette on its own — and a consumer can now override it, app-wide or per instance, exactly as they can a built-in.

Restyle a built-in

Register a different colours object on the theme to recolour a built-in everywhere, or pass one on a node to recolour a single instance.

App-wide

Recolour a built-in by registering a different colours object on the theme with override(). Start from the default derivation and change only what you want with with():

php
use Phui\Colours\Hex;
use Phui\Components\Colours\ButtonColours;

$palette = Palette::nord();

Screen::mount(App::class)
    ->theme(
        Theme::make($palette)->override(
            ButtonColours::for($palette)
                ->with(borderFocused: Hex::from('#a3be8c')),
        ),
    )
    ->run();

ButtonColours::for($palette) is the default mapping of palette roles to the button's parts; with() copies it, replacing only the named values. Chain override() calls to restyle several components; anything you don't override falls back to for($palette).

Each page under Built-in Components lists that component's colours class, what every colour paints and the palette role it defaults to.

One instance

To restyle a single mounted instance — a built-in or one of your own re-themeable components — pass a colours object on its node where the parent mounts it. $this->colours() gives the resolved colours to tweak, so an instance override stacks on top of the theme:

php
public function build(): Node
{
    $palette = $this->themes->palette();

    return Container::make([
        Slider::mount('volume'),
        Slider::mount('danger')->colours(
            $this->colours(SliderColours::class)->with(fill: $palette->danger),
        ),
    ]);
}

Released under the MIT License.