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
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
endWindow props
| Prop | Type | Default | Purpose |
|---|---|---|---|
title | string | n/a | Title bar text |
size | {w, h} | n/a | Initial size in pixels |
width | number | n/a | Width (alternative to size) |
height | number | n/a | Height (alternative to size) |
position | {x, y} | n/a | Initial position |
min_size | {w, h} | n/a | Minimum dimensions |
max_size | {w, h} | n/a | Maximum dimensions |
maximized | boolean | false | Start maximized |
fullscreen | boolean | false | Start fullscreen |
visible | boolean | true | Whether window is visible |
resizable | boolean | true | Allow resizing |
closeable | boolean | true | Show close button |
minimizable | boolean | true | Allow minimizing |
decorations | boolean | true | Show title bar and borders |
transparent | boolean | false | Transparent window background |
blur | boolean | false | Blur window background |
level | atom | :normal | Stacking level (:normal, :always_on_top, :always_on_bottom) |
exit_on_close_request | boolean | true | Whether closing exits the app |
scale_factor | number | n/a | DPI scale override |
theme | atom or map | n/a | Per-window theme (:dark, :nord, :system, or custom) |
a11y | map | n/a | Accessibility 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
]
endexit_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:
| Value | Behaviour |
|---|---|
:shrink | Take only as much space as the content needs. This is the default for most widgets. |
:fill | Take all available space in the parent container. |
{:fill_portion, n} | Take a proportional share of available space relative to siblings. |
| number | Exact 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
endSidebar 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:
- Fixed-size children (explicit pixel values) are measured first
:shrinkchildren are measured at their intrinsic content size: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
Padding is the space between a container's edges and its content. Multiple input forms are accepted:
| Input | Result |
|---|---|
16 | 16px on all sides |
{8, 16} | 8px top/bottom, 16px left/right |
%{top: 16, bottom: 8} | Per-side (unset sides default to 0) |
| Do-block | padding 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
endSpacing applies between children, not before the first or after the last. It does not interact with padding; they are independent.
Alignment
Alignment controls how children are positioned within a container's available space.
| Prop | Container | Values |
|---|---|---|
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.
| Prop | Type | Default | Purpose |
|---|---|---|---|
spacing | number | 0 | Vertical gap between children |
padding | Padding | 0 | Inner padding |
width | Length | :shrink | Column width |
height | Length | :shrink | Column height |
max_width | number | n/a | Maximum width in pixels |
align_x | :left | :center | :right | :left | Horizontal alignment of children |
clip | boolean | false | Clip children that overflow |
wrap | boolean | false | Wrap children to next column on overflow |
a11y | map | n/a | Accessibility 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.
| Prop | Type | Default | Purpose |
|---|---|---|---|
spacing | number | 0 | Horizontal gap between children |
padding | Padding | 0 | Inner padding |
width | Length | :shrink | Row width |
height | Length | :shrink | Row height |
max_width | number | n/a | Maximum width in pixels |
align_y | :top | :center | :bottom | :top | Vertical alignment of children |
clip | boolean | false | Clip children that overflow |
wrap | boolean | false | Wrap children to next row on overflow |
a11y | map | n/a | Accessibility 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.
| Prop | Type | Default | Purpose |
|---|---|---|---|
padding | Padding | 0 | Inner padding |
width | Length | :shrink | Container width |
height | Length | :shrink | Container height |
max_width | number | n/a | Maximum width |
max_height | number | n/a | Maximum height |
align_x | :left | :center | :right | :left | Horizontal child alignment |
align_y | :top | :center | :bottom | :top | Vertical child alignment |
center | boolean | false | Center child in both axes |
clip | boolean | false | Clip child that overflows |
background | Color or Gradient | n/a | Background fill |
color | Color | n/a | Text colour override |
border | Border | n/a | Border specification |
shadow | Shadow | n/a | Drop shadow |
style | atom or StyleMap | n/a | Named preset or full style map |
a11y | map | n/a | Accessibility 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.
| Prop | Type | Default | Purpose |
|---|---|---|---|
width | Length | :shrink | Scrollable area width |
height | Length | :shrink | Scrollable area height |
direction | :vertical | :horizontal | :both | :vertical | Scroll direction |
spacing | number | n/a | Gap between scrollbar and content |
scrollbar_width | number | n/a | Scrollbar track width |
scrollbar_margin | number | n/a | Margin around scrollbar |
scroller_width | number | n/a | Scroller handle width |
scrollbar_color | Color | n/a | Scrollbar track colour |
scroller_color | Color | n/a | Scroller thumb colour |
anchor | :start | :end | :start | Scroll anchor position |
on_scroll | boolean | false | Emit scroll events with viewport data |
auto_scroll | boolean | false | Auto-scroll to show new content |
a11y | map | n/a | Accessibility 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.
| Prop | Type | Default | Purpose |
|---|---|---|---|
width | Length | :shrink | Stack width |
height | Length | :shrink | Stack height |
clip | boolean | false | Clip children that overflow |
a11y | map | n/a | Accessibility 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.
| Prop | Type | Default | Purpose |
|---|---|---|---|
columns | pos_integer | 1 | Number of columns |
spacing | number | 0 | Gap between cells |
width | number | n/a | Grid width in pixels |
height | number | n/a | Grid height in pixels |
column_width | Length | n/a | Width of each column |
row_height | Length | n/a | Height of each row |
fluid | number | n/a | Max cell width for fluid auto-wrap mode |
a11y | map | n/a | Accessibility 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.
| Prop | Type | Default | Purpose |
|---|---|---|---|
x | number | 0 | X position in pixels |
y | number | 0 | Y position in pixels |
width | Length | :shrink | Pin container width |
height | Length | :shrink | Pin container height |
a11y | map | n/a | Accessibility 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.
| Prop | Type | Default | Purpose |
|---|---|---|---|
translate_x | number | 0 | Horizontal translation in pixels |
translate_y | number | 0 | Vertical translation in pixels |
scale | number | n/a | Scale factor |
width | Length | n/a | Container width |
height | Length | n/a | Container height |
a11y | map | n/a | Accessibility 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.
| Prop | Type | Default | Purpose |
|---|---|---|---|
width | Length | :fill | Container width |
height | Length | :fill | Container height |
a11y | map | n/a | Accessibility 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.
| Prop | Type | Default | Purpose |
|---|---|---|---|
width | Length | :shrink | Space width |
height | Length | :shrink | Space height |
a11y | map | n/a | Accessibility 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).
| Prop | Type | Default | Purpose |
|---|---|---|---|
spacing | number | 2 | Space between panes |
width | Length | :fill | Grid width |
height | Length | :fill | Grid height |
min_size | number | 10 | Minimum pane size in pixels |
leeway | number | min_size | Grabbable area around dividers |
divider_color | Color | n/a | Divider colour |
divider_width | number | n/a | Divider thickness |
a11y | map | n/a | Accessibility overrides. See Accessibility. |
Pane grid emits these events:
| Event type | Data | Description |
|---|---|---|
:pane_clicked | pane | Pane selected |
:pane_resized | split, ratio | Divider moved |
:pane_dragged | pane, target, action, region, edge | Pane drag (picked/dropped/canceled) |
:pane_focus_cycle | n/a | F6/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
Sidebar + content
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
endHeader / body / footer
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
endCentred content
container width: :fill, height: :fill, center: true do
text("msg", "Centred in both axes")
endScrollable 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
endOverlay / 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
endSee also
- Layout guide - sizing, spacing, and alignment applied to the pad
- Styling reference - Border, Shadow, and background for containers
- Scoped IDs reference - how named containers create ID scopes
- Built-in Widgets reference - full widget catalog including non-layout widgets
Plushie.Type.Length- Length type encodingPlushie.Type.Padding- Padding normalisation and encodingPlushie.Type.Alignment- alignment value encoding