Every Plushie app starts with a window. Inside it, you compose layout containers to arrange widgets on screen. This reference covers windows, sizing, spacing, alignment, and all layout containers.

Window

Plushie.Widget.Window

The top-level container. Every view/1 must return one or more window nodes. Windows map to native OS windows, each with its own title bar, size, position, and optional theme.

window "main", title: "My App", theme: :dark do
  column width: :fill, height: :fill do
    # app content
  end
end

Window props

PropTypeDefaultPurpose
titlestringn/aTitle bar text
size{w, h}n/aInitial size in pixels
widthnumbern/aWidth (alternative to size)
heightnumbern/aHeight (alternative to size)
position{x, y}n/aInitial position
min_size{w, h}n/aMinimum dimensions
max_size{w, h}n/aMaximum dimensions
maximizedbooleanfalseStart maximized
fullscreenbooleanfalseStart fullscreen
visiblebooleantrueWhether window is visible
resizablebooleantrueAllow resizing
closeablebooleantrueShow close button
minimizablebooleantrueAllow minimizing
decorationsbooleantrueShow title bar and borders
transparentbooleanfalseTransparent window background
blurbooleanfalseBlur window background
levelatom:normalStacking level (:normal, :always_on_top, :always_on_bottom)
exit_on_close_requestbooleantrueWhether closing exits the app
scale_factornumbern/aDPI scale override
themeatom or mapn/aPer-window theme (:dark, :nord, :system, or custom)
a11ymapn/aAccessibility overrides

Multi-window

Return multiple windows from view/1:

def view(model) do
  [
    window "main", title: "App" do
      main_content(model)
    end,

    if model.show_settings do
      window "settings", title: "Settings", exit_on_close_request: false do
        settings_content(model)
      end
    end
  ]
end

exit_on_close_request: false on secondary windows means closing them removes the window without exiting the app. Window IDs must be stable strings. A changing ID causes a close and re-open.

See App Lifecycle for daemon mode (keep running after all windows close) and App Lifecycle for how the runtime syncs window state.

Sizing

Every widget has width: and height: props that control how much space it occupies. Four value forms are supported:

ValueBehaviour
:shrinkTake only as much space as the content needs. This is the default for most widgets.
:fillTake all available space in the parent container.
{:fill_portion, n}Take a proportional share of available space relative to siblings.
numberExact pixel size.

How fill_portion works

When multiple siblings use :fill or {:fill_portion, n}, the available space (after fixed-size and :shrink siblings are measured) is divided proportionally. The numbers are relative ratios:

row width: :fill do
  container "sidebar", width: {:fill_portion, 1} do ... end
  container "main", width: {:fill_portion, 3} do ... end
end

Sidebar gets 1/4 of the width, main gets 3/4. {:fill_portion, 1} and {:fill_portion, 3} is the same ratio as {:fill_portion, 2} and {:fill_portion, 6}.

:fill is shorthand for {:fill_portion, 1}. Two :fill siblings split space equally.

Sizing resolution order

The layout engine processes siblings in this order:

  1. Fixed-size children (explicit pixel values) are measured first
  2. :shrink children are measured at their intrinsic content size
  3. :fill / {:fill_portion, n} children divide the remaining space

This means a fixed-width sidebar always gets its pixels, a shrink button takes what it needs, and fill containers expand to use whatever is left.

Constraints

max_width: and max_height: set upper bounds. A :fill child with max_width: 600 expands to fill available space but never exceeds 600 pixels. These are available on column, row, container, and keyed_column.

Padding

Plushie.Type.Padding

Padding is the space between a container's edges and its content. Multiple input forms are accepted:

InputResult
1616px on all sides
{8, 16}8px top/bottom, 16px left/right
%{top: 16, bottom: 8}Per-side (unset sides default to 0)
Do-blockpadding do top 16; bottom 8 end

Padding reduces the space available to children. A 200px-wide container with padding: 16 has 168px of content space.

Spacing

Spacing is the gap between sibling children inside a container. Set via the spacing: prop on column, row, grid, and keyed_column:

column spacing: 12 do
  text("a", "First")    # 12px gap below
  text("b", "Second")   # 12px gap below
  text("c", "Third")    # no gap after last child
end

Spacing applies between children, not before the first or after the last. It does not interact with padding; they are independent.

Alignment

Plushie.Type.Alignment

Alignment controls how children are positioned within a container's available space.

PropContainerValues
align_x:column, container:left (default), :center, :right
align_y:row, container:top (default), :center, :bottom

column aligns children horizontally (they already stack vertically). row aligns children vertically (they already flow horizontally). container supports both axes since it has a single child.

The center: true shorthand on container sets both align_x: :center and align_y: :center.

Layout containers

column

Arranges children vertically, top to bottom.

PropTypeDefaultPurpose
spacingnumber0Vertical gap between children
paddingPadding0Inner padding
widthLength:shrinkColumn width
heightLength:shrinkColumn height
max_widthnumbern/aMaximum width in pixels
align_x:left | :center | :right:leftHorizontal alignment of children
clipbooleanfalseClip children that overflow
wrapbooleanfalseWrap children to next column on overflow
a11ymapn/aAccessibility overrides. See Accessibility.

wrap: true enables multi-column flow layout. When children exceed the column height, they wrap to a new column to the right (like CSS flex-wrap).

row

Arranges children horizontally, left to right.

PropTypeDefaultPurpose
spacingnumber0Horizontal gap between children
paddingPadding0Inner padding
widthLength:shrinkRow width
heightLength:shrinkRow height
max_widthnumbern/aMaximum width in pixels
align_y:top | :center | :bottom:topVertical alignment of children
clipbooleanfalseClip children that overflow
wrapbooleanfalseWrap children to next row on overflow
a11ymapn/aAccessibility overrides. See Accessibility.

wrap: true enables multi-row flow layout. Useful for tag clouds, toolbar buttons, or any content that should reflow at different widths.

container

Single-child wrapper for styling, scoping, and alignment.

PropTypeDefaultPurpose
paddingPadding0Inner padding
widthLength:shrinkContainer width
heightLength:shrinkContainer height
max_widthnumbern/aMaximum width
max_heightnumbern/aMaximum height
align_x:left | :center | :right:leftHorizontal child alignment
align_y:top | :center | :bottom:topVertical child alignment
centerbooleanfalseCenter child in both axes
clipbooleanfalseClip child that overflows
backgroundColor or Gradientn/aBackground fill
colorColorn/aText colour override
borderBordern/aBorder specification
shadowShadown/aDrop shadow
styleatom or StyleMapn/aNamed preset or full style map
a11ymapn/aAccessibility overrides. See Accessibility.

Container style presets: :transparent, :rounded_box, :bordered_box, :dark, :primary, :secondary, :success, :danger, :warning.

Container serves three roles: styling (background, border, shadow), scoping (named containers create ID scopes for their children; see Scoped IDs), and alignment (positioning a child within available space).

scrollable

Adds scroll bars when content overflows. Requires an explicit string ID because the renderer tracks scroll position as internal state.

PropTypeDefaultPurpose
widthLength:shrinkScrollable area width
heightLength:shrinkScrollable area height
direction:vertical | :horizontal | :both:verticalScroll direction
spacingnumbern/aGap between scrollbar and content
scrollbar_widthnumbern/aScrollbar track width
scrollbar_marginnumbern/aMargin around scrollbar
scroller_widthnumbern/aScroller handle width
scrollbar_colorColorn/aScrollbar track colour
scroller_colorColorn/aScroller thumb colour
anchor:start | :end:startScroll anchor position
on_scrollbooleanfalseEmit scroll events with viewport data
auto_scrollbooleanfalseAuto-scroll to show new content
a11ymapn/aAccessibility overrides. See Accessibility.

auto_scroll: true is useful for chat-style interfaces where new messages should scroll into view. anchor: :end starts scrolled to the bottom.

When on_scroll: true, scroll events include viewport metrics: absolute_x, absolute_y, relative_x, relative_y, bounds (visible area as {width, height}), and content_bounds (full content as {width, height}).

keyed_column

Like column, but uses each child's ID as a diffing key for the renderer. Same props as column minus align_x, clip, and wrap.

Use keyed_column for dynamic lists where items are added, removed, or reordered. A plain column diffs by position index, so adding an item at the top shifts all widget state down by one. keyed_column matches by ID, preserving widget state (focus, scroll position, cursor) regardless of position changes.

stack

Layers children on top of each other (z-axis). First child is at the back, last child is at the front.

PropTypeDefaultPurpose
widthLength:shrinkStack width
heightLength:shrinkStack height
clipbooleanfalseClip children that overflow
a11ymapn/aAccessibility overrides. See Accessibility.

Use for overlays, badges, loading spinners, or any situation where elements need to be layered.

grid

Arranges children in a grid layout.

PropTypeDefaultPurpose
columnspos_integer1Number of columns
spacingnumber0Gap between cells
widthnumbern/aGrid width in pixels
heightnumbern/aGrid height in pixels
column_widthLengthn/aWidth of each column
row_heightLengthn/aHeight of each row
fluidnumbern/aMax cell width for fluid auto-wrap mode
a11ymapn/aAccessibility overrides. See Accessibility.

Two modes: fixed columns (columns: 3) and fluid (fluid: 200). In fluid mode, columns auto-wrap based on available width. The number adjusts to fit as many cells of the specified max width as possible.

pin

Positions a child at exact pixel coordinates within a container.

PropTypeDefaultPurpose
xnumber0X position in pixels
ynumber0Y position in pixels
widthLength:shrinkPin container width
heightLength:shrinkPin container height
a11ymapn/aAccessibility overrides. See Accessibility.

Pin does not participate in flow layout. The child is positioned absolutely. Useful for tooltips, popovers, or custom positioning.

floating

Applies translate and scale transforms to a child.

PropTypeDefaultPurpose
translate_xnumber0Horizontal translation in pixels
translate_ynumber0Vertical translation in pixels
scalenumbern/aScale factor
widthLengthn/aContainer width
heightLengthn/aContainer height
a11ymapn/aAccessibility overrides. See Accessibility.

Unlike pin, floating applies visual transforms without removing the child from flow layout. The child still occupies its original space; the transform is visual only.

responsive

Adapts layout based on available size by emitting resize events.

PropTypeDefaultPurpose
widthLength:fillContainer width
heightLength:fillContainer height
a11ymapn/aAccessibility overrides. See Accessibility.

When the responsive container's size changes, it emits %WidgetEvent{type: :resize, data: %{width: w, height: h}}. Use this in update/2 to store the measured size and adjust your view/1 based on it (for example, switching from a sidebar layout to a stacked layout below a certain width).

space

Invisible spacer widget. No children, no visual output.

PropTypeDefaultPurpose
widthLength:shrinkSpace width
heightLength:shrinkSpace height
a11ymapn/aAccessibility overrides. See Accessibility.

Use for explicit gaps, alignment tricks, or pushing siblings apart in a row or column.

pane_grid

Resizable tiled panes with split, close, swap, and drag. Requires an explicit ID (holds renderer-side state for pane sizes).

PropTypeDefaultPurpose
spacingnumber2Space between panes
widthLength:fillGrid width
heightLength:fillGrid height
min_sizenumber10Minimum pane size in pixels
leewaynumbermin_sizeGrabbable area around dividers
divider_colorColorn/aDivider colour
divider_widthnumbern/aDivider thickness
a11ymapn/aAccessibility overrides. See Accessibility.

Pane grid emits these events:

Event typeDataDescription
:pane_clickedpanePane selected
:pane_resizedsplit, ratioDivider moved
:pane_draggedpane, target, action, region, edgePane drag (picked/dropped/canceled)
:pane_focus_cyclen/aF6/Shift+F6 focus cycling

Manage pane layout with commands: Command.pane_split/4, Command.pane_close/2, Command.pane_swap/3, Command.pane_maximize/2, Command.pane_restore/1.

Composition patterns

row width: :fill, height: :fill do
  column width: 200, height: :fill, padding: 8 do
    # Fixed-width sidebar
  end
  container "main", width: :fill, height: :fill, padding: 16 do
    # Content fills remaining space
  end
end
column width: :fill, height: :fill, spacing: 0 do
  row padding: 8 do
    # Header (shrinks to content)
  end
  container "body", width: :fill, height: :fill do
    # Body (fills remaining space)
  end
  row padding: 8 do
    # Footer (shrinks to content)
  end
end

Centred content

container width: :fill, height: :fill, center: true do
  text("msg", "Centred in both axes")
end

Scrollable list

scrollable "items", height: 400 do
  keyed_column spacing: 4 do
    for item <- model.items do
      container item.id, padding: 8 do
        text(item.id <> "-name", item.name)
      end
    end
  end
end

Overlay / badge

stack do
  container width: :fill, height: :fill do
    # Main content underneath
  end
  pin x: 10, y: 10 do
    text("badge", "NEW", size: 10)
  end
end

See also