Search documentation of Plushie

# Styling

Copy Markdown View Source

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
    end

Some 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
    end

Try 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
      # ...
    end

You 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.

Custom themes also accept `cursor_color`, `scrollbar_color`, and `scroller_color` colour tokens. They are validated like the palette colours and sent with the theme. Scrollbar tokens become scrollable defaults, while widget scrollbar props still win. `cursor_color` applies to focused text entry widgets unless explicit widget text styling overrides it.

## 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
    end

This 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
    end

## Per-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")
    end

Border supports per-corner radius:

    Border.new()
    |> Border.width(1)
    |> Border.color("#ccc")
    |> Border.rounded(Border.radius(8, 8, 0, 0))  # rounded top, square bottom

These 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")
    end

## Design 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
    end

Then use it in your views:

    import PlushiePad.Design

    column spacing: spacing(:md), padding: spacing(:lg) do
      text("title", "Experiments", size: font_size(:lg))
      # ...
    end

This 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"]
      ]
    end

Fonts 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
    end

The 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")
    end

Styling 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 `StyleMap` with 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 `themer` widgets 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.

Next: Animation and Transitions

← Previous Page Layout
Next Page → Animation and Transitions