Ergonomic builder layer for Plushie UI trees.
Import this module in your view/1 function to get concise widget builder
syntax with optional do block sugar for children.
Usage
def view(model) do
import Plushie.UI
window "main", title: "Counter" do
column do
text("Count: #{model.count}")
row do
button("increment", "+")
button("decrement", "-")
end
end
end
endNode shape
Every builder produces:
%{id: string, type: string, props: %{}, children: []}Props use atom keys internally; string keys are used only at the wire encoding
boundary. Reserved opts keys (:children, :id, :do) are not treated as props.
Two equivalent forms
The do block is sugar that compiles to the explicit :children form:
column(padding: 8, children: [text("hello")])
column padding: 8 do
text("hello")
endInside a do block:
forcomprehensions work (they return lists; one level is flattened)ifwithoutelseworks (returns nil; nils are filtered out)
Auto-IDs
WARNING: Auto-generated IDs are unstable. Layout and display widgets
that do not receive an explicit :id option generate one from the call
site line number: "auto:ModuleName:42". These IDs change whenever you
refactor code, add/remove lines above the call, or use conditional
rendering (if/case) that moves the call to a different branch.
When an ID changes between renders, the renderer treats it as a removal + insertion, losing all widget-local state (scroll position, text cursor, focus, editor content).
Always supply explicit :id opts for stateful widgets:
text_editor, combo_box, pane_grid, scrollable, text_input.
Auto-IDs are fine for purely visual widgets like text, row, column
where state loss is invisible.
Formatter
Plushie exports formatter settings that keep layout blocks paren-free so
they read like declarative markup. Add :plushie to import_deps in
your .formatter.exs:
# .formatter.exs
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
import_deps: [:plushie]
]Layout blocks stay paren-free; leaf widgets keep parens for clarity:
column padding: 8 do
text("count", "Count: #{model.count}", size: 24)
button("inc", "+1")
endContainer inline props
Container widgets (column, row, container, etc.) support option
declarations directly inside their do-blocks, mixed with children:
column do
spacing 8
padding do
top 16
bottom 16
end
width :fill
text("Hello")
button("save", "Save")
endOptions and children can be freely mixed. Options are validated at compile time -- using an option that doesn't belong to the container produces a helpful error.
Struct-typed options support nested do-blocks:
container "hero" do
border do
width 1
color "#ddd"
rounded 4
end
shadow do
color "#00000022"
offset_y 2
blur_radius 4
end
padding 20
text("Welcome")
endAll three forms are equivalent and can be mixed:
column spacing: 8, padding: 16 do ... end # keyword on call line
column do spacing 8; padding 16; ... end # inline in block
column do padding do top 16 end; ... end # nested do-blockBlock-form options
Leaf widgets accept an optional do block for setting props when the
keyword list gets long:
button "save", "Save" do
style(:primary)
endThe keyword form is still valid and preferred for short option lists.
Canvas shapes
Canvas shape functions (rect, circle, line, path, stroke,
linear_gradient, move_to, line_to, etc.) and canvas structure
macros (group, layer) are available directly via import Plushie.UI.
No separate import Plushie.Canvas.Shape is needed inside canvas blocks.
Inside canvas, layer, and group blocks, text, image, and
svg calls resolve automatically to their canvas shape variants.
Interactive fields go directly on the group via keyword opts:
canvas "toggle", width: 52, height: 28 do
layer "bg" do
group "switch", on_click: true, cursor: :pointer do
rect(0, 0, 52, 28, fill: "#4CAF50", radius: 14)
circle(36, 14, 10, fill: "#fff")
end
end
endImport Plushie.Canvas.Shape directly only when building shapes in
helper functions outside canvas blocks.
Prop override semantics
When both the keyword argument on the call line and a block declaration specify the same option, the block value wins:
column spacing: 8 do
spacing 16 # overrides -- spacing is 16
text("hello")
endThis applies to all container and leaf widget do-blocks.
Control flow preservation
All DSL blocks preserve every expression from control flow forms.
Multi-expression if/for/case/cond/with bodies contribute
all their expressions to the parent's children list, not just the
last one.
Tree query
find/2, find/3, and find_local/2 are re-exported from
Plushie.Tree for convenience.
find/2 does exact scoped lookup. Use find_local/2 when you
intentionally want a local ID search:
Plushie.UI.find(tree, "form/save")
Plushie.UI.find(tree, "save", "settings")
Plushie.UI.find_local(tree, "save")Internals
For maintainer and widget author details on the macro architecture,
see docs/reference/dsl.md.
Summary
Functions
Clickable button.
Canvas for drawing shapes organized into named layers.
Boolean checkbox toggle.
Builds a circle shape. See Plushie.Canvas.Shape.circle/4.
Vertical flex layout.
Combo box with free-text input and dropdown suggestions.
Generic box with alignment and padding.
Returns true if a node with id exists in the tree.
Finds the first node in a tree whose :id matches id.
Finds all nodes matching a predicate.
Floating overlay layout.
Grid layout.
Groups child shapes with optional positioning and interaction.
Returns all node IDs in the tree.
Raster image display.
Keyed column for efficient list diffing.
Collects shapes into a named layer for use inside canvas blocks.
Builds a line shape. See Plushie.Canvas.Shape.line/5.
Creates a looping transition descriptor.
Markdown content renderer.
Overlay container. First child is the anchor, second is the overlay content.
Pane grid for resizable tiled panes.
Builds a path shape. See Plushie.Canvas.Shape.path/2.
Dropdown pick list for selecting from a list of options.
Pin layout for absolute positioning.
Pointer area for capturing mouse events on children.
Progress indicator.
QR code display. No children.
Radio button for single-value selection from a group.
Builds a rectangle shape. See Plushie.Canvas.Shape.rect/5.
Responsive layout that adapts to available size.
Rich text display with styled spans.
Horizontal flex layout.
Horizontal or vertical divider.
Scrollable region.
Sensor for detecting layout changes on children.
Creates a sequential animation chain.
Horizontal slider for numeric range input.
Flexible spacer. No children.
Creates a physics-based spring descriptor.
Z-axis stacking layout (overlays).
Builds a stroke descriptor. See Plushie.Canvas.Shape.stroke/3.
SVG image display.
Data table widget.
Text label.
Multi-line text editor.
Single-line text input.
Per-subtree theme override.
Toggle switch.
Tooltip wrapper. Children are the content being tooltipped.
Creates a timed transition descriptor for animated prop values.
Vertical slider for numeric range input.
Top-level window container.
Functions
Clickable button.
Emits %WidgetEvent{type: :click, id: id} when clicked.
Example
button("save", "Save", style: :primary)
button "save", "Save" do
style :primary
end
Canvas for drawing shapes organized into named layers.
Keyword form
canvas("drawing",
layers: %{"main" => [%{type: "circle", x: 50, y: 50, r: 20}]},
width: 400,
height: 300
)Do-block form
Use layer/2 to collect layers declaratively:
canvas "chart", width: 400, height: 300 do
layer "grid" do
rect(0, 0, 400, 300, stroke: "#eee")
end
layer "data" do
for bar <- bars do
rect(bar.x, bar.y, bar.w, bar.h, fill: bar.color)
end
end
endOptions
:layers-- map of layer names to shape descriptor lists:width/:height-- dimensions:background-- background color
Boolean checkbox toggle.
Emits %WidgetEvent{type: :toggle, id: id, value: boolean} when toggled.
Example
checkbox("agree", model.agreed, label: "I agree")
checkbox "agree", model.agreed do
label "I agree"
end
Builds a circle shape. See Plushie.Canvas.Shape.circle/4.
Vertical flex layout.
Options
:spacing-- gap between children:padding-- padding around children:width/:height--:fill,:shrink, or number:align_x--:left,:center,:right:id-- explicit ID (otherwise auto-generated from call site):children-- child nodes (function-form shorthand)
Example
column spacing: 8 do
text("Hello")
text("World")
end
Combo box with free-text input and dropdown suggestions.
Example
combo_box("lang", ["Elixir", "Rust", "Go"], model.lang, placeholder: "Type...")
combo_box "lang", ["Elixir", "Rust", "Go"], model.lang do
placeholder "Type..."
end
Generic box with alignment and padding.
Example
container "hero", padding: 16 do
text("Welcome")
end
@spec exists?(tree :: Plushie.Widget.ui_node() | nil, id :: String.t()) :: boolean()
Returns true if a node with id exists in the tree.
@spec find(tree :: Plushie.Widget.ui_node(), id :: String.t()) :: Plushie.Widget.ui_node() | nil
Finds the first node in a tree whose :id matches id.
Delegates to Plushie.Tree.find/2. Returns the node map or nil.
Example
tree = MyApp.view(model)
Plushie.UI.find(tree, "save_button")
@spec find( tree :: Plushie.Widget.ui_node(), id :: String.t(), window_id :: String.t() ) :: Plushie.Widget.ui_node() | nil
See Plushie.Tree.find/3.
@spec find_all( tree :: Plushie.Widget.ui_node() | nil, id_or_pred :: String.t() | (Plushie.Widget.ui_node() -> boolean()) ) :: [Plushie.Widget.ui_node()]
Finds all nodes matching a predicate.
Floating overlay layout.
Example
floating "popup" do
text("Floating content")
end
Grid layout.
Options
:columns-- number of columns:column_width-- width of each column:row_height-- height of each row:spacing-- gap between cells:padding-- padding around grid:width/:height-- dimensions:id-- explicit ID (otherwise auto-generated from call site)
Example
grid columns: 3, spacing: 8 do
for item <- items do
text(item.name)
end
end
Groups child shapes with optional positioning and interaction.
Do-block form
group "btn", x: 4, y: 4, on_click: true do
rect(0, 0, 32, 32, radius: 4)
endList form
group([rect(0, 0, 100, 40)], x: 10, y: 50)
@spec ids(tree :: Plushie.Widget.ui_node() | nil) :: [String.t()]
Returns all node IDs in the tree.
Raster image display.
Example
image("logo", "/assets/logo.png", width: 200, content_fit: :cover)
image "logo", "/assets/logo.png" do
width 200
content_fit :cover
end
Keyed column for efficient list diffing.
Options
Same as column/1.
Example
keyed_column spacing: 8 do
for item <- items do
text(item.id, item.name)
end
end
Collects shapes into a named layer for use inside canvas blocks.
canvas "chart", width: 400 do
layer "grid" do
rect(0, 0, 400, 300, stroke: "#eee")
end
end
Builds a line shape. See Plushie.Canvas.Shape.line/5.
Creates a looping transition descriptor.
Sets repeat: :forever and auto_reverse: true by default.
Requires from: and to:.
Examples
opacity: loop(800, to: 0.4, from: 1.0)
rotation: loop(1000, to: 360, from: 0, auto_reverse: false)
opacity: loop(to: 0.4, from: 1.0, duration: 800, cycles: 3)
Markdown content renderer.
Forms
markdown(content)-- auto-generated IDmarkdown(id, content)-- explicit IDmarkdown(id, content, opts)-- explicit ID with options
Example
markdown("# Hello\n\nSome **bold** text")
markdown("my_md", "# Hello", code_theme: "dracula")
Overlay container. First child is the anchor, second is the overlay content.
Options
:position--:below,:above,:left,:right:gap-- space between anchor and overlay in pixels:offset_x-- horizontal offset in pixels:offset_y-- vertical offset in pixels
Example
overlay "popup", position: :below, gap: 4 do
button("anchor", "Click me")
container "dropdown" do
text("dropdown_text", "Dropdown content")
end
end
Pane grid for resizable tiled panes.
Options
:spacing-- gap between panes:min_size-- minimum pane size:on_resize-- resize event tag:on_drag-- drag event tag:on_click-- click event tag
Children are pane content keyed by ID.
Example
pane_grid "editor_panes", spacing: 2 do
column id: "left" do
text("Left pane")
end
column id: "right" do
text("Right pane")
end
end
Builds a path shape. See Plushie.Canvas.Shape.path/2.
Dropdown pick list for selecting from a list of options.
Example
pick_list("country", ["UK", "US", "DE"], model.country, placeholder: "Choose...")
pick_list "country", ["UK", "US", "DE"], model.country do
placeholder "Choose..."
end
Pin layout for absolute positioning.
Example
pin "overlay" do
text("Pinned content")
end
Pointer area for capturing mouse events on children.
Options
:on_press,:on_release,:on_right_press,:on_middle_press:on_enter,:on_exit
Example
pointer_area "clickable" do
text("Click me")
end
Progress indicator.
Forms
progress_bar(range, value)-- auto-generated ID (sugar)progress_bar(id, range, value)-- explicit IDprogress_bar(id, range, value, opts)-- explicit ID with options
Arguments
range--{min, max}tuple defining the full rangevalue-- current value within the range
Example
progress_bar({0, 100}, model.progress)
progress_bar("dl_progress", {0, 100}, model.progress, height: 8)
QR code display. No children.
Arguments
id-- unique identifierdata-- the string to encode
Options
:cell_size-- size of each QR module in pixels (default 4.0):cell_color-- color of dark modules:background-- color of light modules:error_correction--:low,:medium(default),:quartile,:high
Example
qr_code("my_qr", "https://example.com", cell_size: 6)
qr_code "my_qr", "https://example.com" do
cell_size 6
end
Radio button for single-value selection from a group.
Use the group option so all radios in the same group emit select events
with the group name as the ID instead of each radio's individual ID.
Example
radio("size_sm", "small", model.size, label: "Small", group: "size")
radio("size_lg", "large", model.size, label: "Large", group: "size")
radio "size_sm", "small", model.size do
label "Small"
group "size"
end
Builds a rectangle shape. See Plushie.Canvas.Shape.rect/5.
Responsive layout that adapts to available size.
Options
:width/:height-- dimensions:id-- explicit ID (otherwise auto-generated from call site)
Example
responsive do
column do
text("Adapts to size")
end
end
Rich text display with styled spans.
Options
:spans-- list of span descriptors:width-- width
Example
rich_text("styled", spans: [%{text: "bold", weight: :bold}, %{text: " normal"}])
rich_text "styled" do
spans [%{text: "bold", weight: :bold}, %{text: " normal"}]
end
Horizontal flex layout.
Options
Same as column/1.
Example
row spacing: 4 do
button("yes", "Yes")
button("no", "No")
end
Horizontal or vertical divider.
Example
rule(width: :fill)
rule do
direction :vertical
style :weak
end
Scrollable region.
Example
scrollable "feed" do
for item <- items do
text(item.title)
end
end
Sensor for detecting layout changes on children.
Options
:on_resize,:on_appear
Example
sensor "tracked" do
text("Monitored content")
end
Creates a sequential animation chain.
Steps execute one after another on the same prop. Accepts a list of transition/spring descriptors.
Examples
opacity: sequence([
transition(200, to: 1.0, from: 0.0),
loop(800, to: 0.7, from: 1.0, cycles: 3),
transition(300, to: 0.0)
])
opacity: sequence do
transition(200, to: 1.0, from: 0.0)
transition(300, to: 0.0)
end
Horizontal slider for numeric range input.
Arguments
range--{min, max}tuple ormin..maxRangevalue-- current value
Example
slider("volume", {0, 100}, model.volume, step: 5)
slider("volume", 0..100, model.volume, step: 5)
slider "volume", {0, 100}, model.volume do
step 5
end
Flexible spacer. No children.
Options
:width--:fill,:shrink, or number:height--:fill,:shrink, or number
Example
space(width: :fill)
space do
width :fill
end
Creates a physics-based spring descriptor.
Springs have no fixed duration -- they settle naturally based on stiffness and damping.
Examples
scale: spring(to: 1.05, preset: :bouncy)
scale: spring(to: 1.05, stiffness: 200, damping: 20)
scale: spring do
to 1.05
preset :bouncy
end
Z-axis stacking layout (overlays).
Example
stack do
image("bg", "/path/to/bg.png")
container "overlay", padding: 16 do
text("Overlaid text")
end
end
Builds a stroke descriptor. See Plushie.Canvas.Shape.stroke/3.
SVG image display.
Example
svg("icon", "/assets/icon.svg", width: 24, height: 24)
svg "icon", "/assets/icon.svg" do
width 24
height 24
end
Data table widget.
Options
:columns-- list of column descriptors (%{key, label, width}):rows-- list of row data maps
The do block can contain row content templates (e.g. for custom cell
rendering). Children from the block are stored in :children.
Examples
table("users", columns: cols, rows: data)
table "users", columns: cols, rows: data do
text("custom footer")
end
Text label.
Forms
text(content)-- auto-generated ID (sugar for quick labels)text(id, content)-- explicit IDtext(id, content, opts)-- explicit ID with options
Example
text("Hello, world!")
text("greeting", "Hello, world!", size: 18)
Multi-line text editor.
Example
text_editor("notes", model.notes, width: :fill, height: 200)
text_editor "notes", model.notes do
width :fill
height 200
end
Single-line text input.
Emits %WidgetEvent{type: :input, id: id, value: value} on change and %WidgetEvent{type: :submit, id: id, value: value} on Enter.
Example
text_input("name", model.name, placeholder: "Your name")
text_input "name", model.name do
placeholder "Your name"
end
Per-subtree theme override.
Options
:theme-- theme name string or custom palette map
Example
themer "dark_section", theme: "Dark" do
column do
text("This subtree uses the dark theme")
end
end
Toggle switch.
Emits %WidgetEvent{type: :toggle, id: id, value: boolean} when toggled.
Example
toggler("dark_mode", model.dark_mode, label: "Dark mode")
toggler "dark_mode", model.dark_mode do
label "Dark mode"
end
Tooltip wrapper. Children are the content being tooltipped.
Forms
tooltip(id, tip, do: block)-- with childrentooltip(id, tip, opts, do: block)-- with children and options
Example
tooltip "save_tip", "Save your work", position: :top do
button("save", "Save")
end
Creates a timed transition descriptor for animated prop values.
The renderer handles interpolation locally -- zero wire traffic during animation. Duration can be a positional argument or a keyword.
Examples
opacity: transition(300, to: 0.0)
opacity: transition(300, to: 0.0, easing: :ease_out)
opacity: transition(to: 0.0, duration: 300)
# Enter animation
opacity: transition(200, to: 1.0, from: 0.0)
# Do-block
opacity: transition 300 do
to 0.0
easing :ease_out
end
Vertical slider for numeric range input.
Same as slider/4 but oriented vertically. Accepts {min, max} or min..max.
Example
vertical_slider("brightness", {0, 100}, model.brightness)
vertical_slider("brightness", 0..100, model.brightness)
vertical_slider "brightness", {0, 100}, model.brightness do
step 1
end
Top-level window container.
Arguments
id-- stable string identifier for this windowopts-- keyword list; common option::title
Example
window "main", title: "My App" do
column do
text("Hello")
end
end