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:
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:
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:
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:
$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:
$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:
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:
/** @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:
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:
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():
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:
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),
),
]);
}