With the layout in place, it is time to make the pad look good. Plushie has a
layered styling system: themes set the overall palette, per-widget styles
override individual elements, and type modules like Plushie.Type.Border and
Plushie.Type.Shadow handle the details.
This chapter covers the parts you will use most often. The full theme palette, shade override keys, and every type option are in the Styling reference.
Themes
Every window has a theme: prop that sets the colour palette for all widgets
inside it. Plushie ships with a set of built-in themes:
window "main", title: "Plushie Pad", theme: :dark do
# All widgets inside use the dark palette
endSome popular options: :dark, :light, :nord, :dracula,
:catppuccin_mocha, :tokyo_night, :gruvbox_dark. See
Plushie.Type.Theme for the complete list.
Use :system to follow the operating system's light/dark preference:
window "main", title: "Plushie Pad", theme: :system do
# Follows OS theme
endTry a few themes on the pad's window to see how the entire UI adapts -- buttons, text inputs, scrollbars, and the editor all respond.
Custom themes
You can create a custom theme by providing a handful of seed colours.
Plushie.Type.Theme.custom/2 generates a full palette from them:
my_theme = Plushie.Type.Theme.custom("My Theme",
primary: "#3b82f6",
danger: "#ef4444",
background: "#1a1a2e",
text: "#e0e0e8"
)
window "main", title: "Plushie Pad", theme: my_theme do
# ...
endYou can also extend a built-in theme, overriding only the colours you want to change:
my_theme = Plushie.Type.Theme.custom("Nord+",
base: :nord,
primary: "#88c0d0"
)For fine-grained control, the theme system supports shade overrides (keys
like primary_strong, background_weakest, danger_base_text) that target
specific shade levels in the generated palette. See Plushie.Type.Theme for
the full list of shade keys.
Subtree theming
The themer widget applies a different theme to its children without
affecting the rest of the window:
window "main", title: "App", theme: :light do
column do
text("light-text", "This is light themed")
themer "dark-section", theme: :dark do
container "sidebar", padding: 12 do
text("dark-text", "This section is dark")
end
end
end
endThis is useful for dark sidebars in a light app, brand-specific sections, or
any case where part of the UI needs a different palette. No prop threading
needed. themer changes the theme context for everything inside it.
You can give the preview pane a different theme from the rest of the pad
using themer, so experiments render in a distinct palette:
themer "preview-theme", theme: :light do
container "preview", width: {:fill_portion, 1}, height: :fill, padding: 8 do
# preview content
end
endPer-widget styling with StyleMap
Themes set the baseline palette. Plushie.Type.StyleMap overrides the
appearance of individual widget instances.
Build a style with the builder pattern:
alias Plushie.Type.{StyleMap, Border, Shadow}
save_style =
StyleMap.new()
|> StyleMap.background("#3b82f6")
|> StyleMap.text_color("#ffffff")
|> StyleMap.hovered(%{background: "#2563eb"})
|> StyleMap.pressed(%{background: "#1d4ed8"})
button("save", "Save", style: save_style)The properties you can set: background (colour or gradient), text_color,
border (Plushie.Type.Border struct), shadow (Plushie.Type.Shadow
struct).
Status overrides
hovered, pressed, disabled, and focused set property overrides for
those interaction states. Each accepts the same properties as the base style:
StyleMap.new()
|> StyleMap.background("#ffffff")
|> StyleMap.border(Border.new() |> Border.color("#e5e7eb") |> Border.width(1))
|> StyleMap.hovered(%{border: Border.new() |> Border.color("#3b82f6") |> Border.width(2)})
|> StyleMap.focused(%{border: Border.new() |> Border.color("#3b82f6") |> Border.width(2)})Named presets
Instead of building from scratch, you can extend a named preset:
StyleMap.new()
|> StyleMap.base(:primary)
|> StyleMap.hovered(%{background: "#2563eb"})Available presets vary by widget. :primary, :secondary, :success,
:danger, :warning, :text are common. Check each widget's module docs
for its supported presets.
You can also use the preset atom directly on the :style prop:
button("save", "Save", style: :primary)
button("cancel", "Cancel", style: :text)Borders and shadows
Plushie.Type.Border and Plushie.Type.Shadow are struct types used
in container styling and StyleMap:
alias Plushie.Type.{Border, Shadow}
card_border =
Border.new()
|> Border.color("#e5e7eb")
|> Border.width(1)
|> Border.rounded(8)
card_shadow =
Shadow.new()
|> Shadow.color("#0000001a")
|> Shadow.offset(0, 2)
|> Shadow.blur_radius(4)
container "card", border: card_border, shadow: card_shadow, padding: 16 do
text("content", "Card content")
endBorder supports per-corner radius:
Border.new()
|> Border.width(1)
|> Border.color("#ccc")
|> Border.rounded(Border.radius(8, 8, 0, 0)) # rounded top, square bottomThese types also work in do-block syntax:
container "card" do
border do
color "#e5e7eb"
width 1
rounded 8
end
shadow do
color "#0000001a"
offset 0, 2
blur_radius 4
end
padding 16
text("content", "Card content")
endDesign tokens
Plushie does not have a built-in design system framework. Instead, you use plain Elixir modules to define consistent values. This pattern works well:
defmodule PlushiePad.Design do
alias Plushie.Type.{StyleMap, Border, Shadow}
# Spacing scale
def spacing(:xs), do: 4
def spacing(:sm), do: 8
def spacing(:md), do: 12
def spacing(:lg), do: 16
def spacing(:xl), do: 24
# Font sizes
def font_size(:sm), do: 12
def font_size(:md), do: 14
def font_size(:lg), do: 18
def font_size(:xl), do: 24
# Border radius
def radius(:sm), do: 4
def radius(:md), do: 8
def radius(:lg), do: 12
# Reusable styles
def card_style do
StyleMap.new()
|> StyleMap.background("#ffffff")
|> StyleMap.border(
Border.new() |> Border.color("#e5e7eb") |> Border.width(1) |> Border.rounded(radius(:md))
)
|> StyleMap.shadow(
Shadow.new() |> Shadow.color("#0000001a") |> Shadow.offset(0, 2) |> Shadow.blur_radius(4)
)
end
endThen use it in your views:
import PlushiePad.Design
column spacing: spacing(:md), padding: spacing(:lg) do
text("title", "Experiments", size: font_size(:lg))
# ...
endThis is just Elixir module design, no Plushie magic. But it keeps spacing, sizes, and styles consistent across your app. As the pad grows, a design module prevents the gradual drift toward inconsistent values.
Fonts
Plushie supports three font forms:
:default- the system default proportional font:monospace- the system monospace font"Family Name"- a specific font family by name
App-level font defaults are set via the Plushie.App.settings/0 callback:
def settings do
[
default_text_size: 14,
fonts: ["/path/to/custom-font.ttf"]
]
endFonts loaded in settings/0 are available by family name in any widget's
font: prop.
Applying it: the styled pad
Put it all together. Set a dark theme on the window, style the save button as primary, highlight errors in red, and add a border between the sidebar and editor:
window "main", title: "Plushie Pad", theme: :dark do
column width: :fill, height: :fill, spacing: 0 do
row width: :fill, height: :fill, spacing: 0 do
# Sidebar with a right border
container "sidebar-wrap",
border: Border.new() |> Border.width(1) |> Border.color("#333") do
file_list(model)
end
text_editor "editor", model.source do
width {:fill_portion, 1}
height :fill
highlight_syntax "ex"
font :monospace
end
container "preview", width: {:fill_portion, 1}, height: :fill, padding: 12 do
if model.error do
text("error", model.error, color: "#ef4444", size: 13)
else
# ...
end
end
end
row padding: {4, 8}, spacing: 8 do
button("save", "Save", style: :primary)
checkbox("auto-save", model.auto_save)
text("auto-label", "Auto-save", size: 12)
end
# ...event log...
end
endThe dark theme transforms the entire pad. The primary save button stands out. The sidebar border creates visual separation. Error text uses an explicit red colour for contrast. Small adjustments, dramatic result.
Verify it
Test that the styled pad still works end-to-end:
test "styled pad compiles and previews correctly" do
click("#save")
assert_text("#preview/greeting", "Hello, Plushie!")
assert_not_exists("#error")
endStyling is visual, but this confirms the theme, borders, and style changes did not break the compilation and preview flow.
Try it
Write a styling experiment in your pad:
- Create a card: container with border, shadow, rounded corners, and padding.
- Try
StyleMapwith status overrides: build a button that changes colour on hover and press. - Try the do-block syntax for border and shadow. Nest them inside a container block.
- Apply different themes to nested
themerwidgets to see how palettes compose. - Build a design token module for your experiments with a spacing scale and reusable styles.
In the next chapter, we will add animations and transitions to make the pad feel alive.