Theming
Plushie exposes iced's theming system directly. No additional abstraction layer, no token system, no design system framework. If you need those, build them in your app.
Setting a theme
Themes are set at the window level:
def view(model)
window("main", title: "My App") do
themer("theme", theme: :catppuccin_mocha) do
column do
text("Themed content")
end
end
end
end
Built-in themes
Iced 0.14 ships with 22 built-in themes. Plushie passes the theme name
string directly to the renderer, which resolves it to an iced Theme
variant.
All 22 built-in themes:
| Name | Description |
|---|---|
light |
Default light theme |
dark |
Default dark theme |
dracula |
Dracula color scheme |
nord |
Nord color scheme |
solarized_light |
Solarized Light |
solarized_dark |
Solarized Dark |
gruvbox_light |
Gruvbox Light |
gruvbox_dark |
Gruvbox Dark |
catppuccin_latte |
Catppuccin Latte (light) |
catppuccin_frappe |
Catppuccin Frappe |
catppuccin_macchiato |
Catppuccin Macchiato |
catppuccin_mocha |
Catppuccin Mocha (dark) |
tokyo_night |
Tokyo Night |
tokyo_night_storm |
Tokyo Night Storm |
tokyo_night_light |
Tokyo Night Light |
kanagawa_wave |
Kanagawa Wave |
kanagawa_dragon |
Kanagawa Dragon |
kanagawa_lotus |
Kanagawa Lotus |
moonfly |
Moonfly |
nightfly |
Nightfly |
oxocarbon |
Oxocarbon |
ferra |
Ferra |
Unknown names fall back to dark.
Custom themes
Custom themes are defined by providing a palette:
theme = Plushie::Type::Theme.custom("my_app",
background: "#1e1e2e",
text: "#cdd6f4",
primary: "#89b4fa",
success: "#a6e3a1",
danger: "#f38ba8",
warning: "#f9e2af"
)
Then pass it to a themer widget:
themer("app_theme", theme: theme) do
# ...
end
The palette is passed to iced's Theme::custom() with Oklch-based
palette generation (plushie-iced). Only the colors you specify are overridden;
the rest are derived automatically.
Extended palette shade overrides
When you set a custom theme, iced generates an "extended palette" of shade variants from your six core colors. If the auto-generated shades don't match your design, you can override individual shades by adding flat keys to the theme hash:
Key naming convention
For the five color families (primary, secondary, success, warning, danger), each has three shade levels:
| Key | What it controls |
|---|---|
{family}_base |
Base shade background |
{family}_weak |
Weak shade background |
{family}_strong |
Strong shade background |
{family}_base_text |
Text color on the base shade |
{family}_weak_text |
Text color on the weak shade |
{family}_strong_text |
Text color on the strong shade |
The background family has eight levels: background_base,
background_weakest, background_weaker, background_weak,
background_neutral, background_strong, background_stronger,
background_strongest. Each also supports a _text suffix.
Example
theme = Plushie::Type::Theme.custom("branded",
background: "#1a1a2e",
text: "#e0e0e0",
primary: "#0f3460",
primary_strong: "#1a5276",
primary_strong_text: "#ffffff",
background_weakest: "#0d0d1a"
)
Shade overrides only apply to custom themes (hash values). Built-in theme
symbols like :dark or :nord are not affected.
Per-subtree theme override
Themes can be overridden for a subtree using a themer wrapper:
column do
text("Uses window theme")
themer("sidebar_theme", theme: :nord) do
container("sidebar") do
text("Uses Nord theme")
end
end
end
This is useful for panels, modals, or sections that need a different visual treatment.
Widget-level styling
Individual widgets accept a style prop. This can be a named preset symbol
or a Plushie::Type::StyleMap for per-instance visual customization.
Named presets
("save", "Save", style: :primary)
("cancel", "Cancel", style: :secondary)
("delete", "Delete", style: :danger)
Style symbols (:primary, :secondary, :danger, etc.) map to iced's
built-in style functions. Available presets vary by widget.
Style maps
Style maps let you fully customize widget appearance from Ruby without writing Rust. They work on all styleable widgets: button, container, text_input, text_editor, checkbox, radio, toggler, pick_list, progress_bar, rule, slider, vertical_slider, and tooltip.
card_style = Plushie::Type::StyleMap::Spec.new(
background: "#ffffff",
text_color: "#1a1a1a",
border: Plushie::Type::Border.from_opts(color: "#e0e0e0", width: 1, rounded: 8),
shadow: Plushie::Type::Shadow.from_opts(color: "#00000020", offset_y: 2, blur_radius: 8)
)
container("card", style: card_style) do
text("Card content")
end
Style map fields
background-- hex color for the widget backgroundtext_color-- hex color for textborder-- aPlushie::Type::Border(color, width, radius)shadow-- aPlushie::Type::Shadow(color, offset, blur_radius)
Status overrides
Style maps support interaction state overrides:
nav_item_style = Plushie::Type::StyleMap::Spec.new(
background: "#00000000",
text_color: "#cccccc",
hovered: {background: "#333333", text_color: "#ffffff"},
pressed: {background: "#222222"},
disabled: {text_color: "#666666"}
)
Supported statuses: hovered, pressed, disabled, focused.
If you don't specify an override for a status, the renderer auto-derives:
- hovered: darkens background by 10%
- pressed: uses the base style (matching iced's own pattern)
- disabled: applies 50% alpha to background and text_color
Presets and style maps together
Style maps don't replace presets -- they complement them:
# Standard danger button
("delete", "Delete", style: :danger)
# Custom branded button
("cta", "Get Started", style:
Plushie::Type::StyleMap::Spec.new(
background: "#7c3aed",
text_color: "#ffffff",
border: Plushie::Type::Border.from_opts(rounded: 24)
)
)
System theme detection
The simplest way to follow the OS light/dark preference is to set the
window theme to :system:
window("main", title: "My App") do
themer("sys_theme", theme: :system) do
# content
end
end
The renderer tracks the current OS mode and applies Light or Dark
automatically. This also works in settings for the app-level default:
def settings = {theme: :system}
For manual control, subscribe to theme change events:
def subscribe(_model)
[Subscription.on_theme_change(:theme_changed)]
end
# ...
in Event::System[type: :theme_changed, data:]
model.with(preferred_theme: data)
Note: The themer widget (per-subtree theme override) does not support
:system as a theme value. Use :system on window nodes or in settings
instead.
Density
For apps that need density-aware spacing, build a simple helper method:
def spacing(density, size)
case [density, size]
in [:compact, :md] then 4
in [:comfortable, :md] then 8
in [:roomy, :md] then 12
end
end
column(spacing: spacing(:compact, :md)) do
# ...
end
There is no global density setting or built-in density module -- your app decides how to handle it.