All built-in widgets are available via import Plushie.UI. Each widget
has a corresponding typed builder module under Plushie.Widget.* for
programmatic use.
Widget catalog
Layout
| DSL macro | Module | Description |
|---|---|---|
window | Plushie.Widget.Window | Top-level window with title, size, position, theme |
column | Plushie.Widget.Column | Arranges children vertically |
row | Plushie.Widget.Row | Arranges children horizontally |
container | Plushie.Widget.Container | Single-child wrapper for styling, scoping, alignment |
scrollable | Plushie.Widget.Scrollable | Scrollable viewport around child content |
stack | Plushie.Widget.Stack | Layers children on top of each other (z-axis) |
grid | Plushie.Widget.Grid | Fixed-column or fluid grid layout |
keyed_column | Plushie.Widget.KeyedColumn | Vertical layout with ID-based diffing for dynamic lists |
responsive | Plushie.Widget.Responsive | Emits resize events for adaptive layouts |
pin | Plushie.Widget.Pin | Positions child at absolute coordinates |
floating | Plushie.Widget.Floating | Applies translate/scale transforms to child |
space | Plushie.Widget.Space | Invisible spacer |
Full prop tables for all layout containers are in the Layout reference.
Input
| DSL macro | Macro form | Events |
|---|---|---|
button | button(id, label) | :click |
text_input | text_input(id, value, opts) | :input, :submit, :paste |
text_editor | text_editor(id, content, opts) | :input |
checkbox | checkbox(id, checked, opts) | :toggle (value: boolean) |
toggler | toggler(id, checked, opts) | :toggle (value: boolean) |
radio | radio(id, value, selected, opts) | :select |
slider | slider(id, range, value, opts) | :slide, :slide_release |
vertical_slider | vertical_slider(id, range, value, opts) | :slide, :slide_release |
pick_list | pick_list(id, options, selected, opts) | :select, :open, :close |
combo_box | combo_box(id, value, opts) | :input, :select, :open, :close |
button is the simplest interactive widget. The label is the second
positional argument. Emits :click on press.
text_input is a single-line editable field. Emits :input on every
keystroke with the full text as value. Emits :submit on Enter when
on_submit: true is set. Emits :paste when on_paste: true is set.
text_editor is a multi-line editable area with syntax highlighting
support (highlight_syntax: "ex"). The content argument seeds the
initial text. Holds renderer-side state (cursor, selection, scroll).
checkbox / toggler are boolean toggles. Both emit :toggle with
the new boolean value. checkbox shows a box; toggler shows a switch.
Add label: for accessible text.
slider / vertical_slider are range inputs. range is a
{min, max} tuple. Emits :slide continuously while dragging and
:slide_release when the drag ends with the final value.
pick_list is a dropdown selection. options is a list of strings.
selected is the currently selected value (or nil). Emits :select
when an option is chosen.
combo_box is a searchable dropdown. Combines a text input with a
filtered option list. Holds renderer-side state (search text, open
state). Emits :input on typing and :select on option selection.
radio is a one-of-many selection. value is the option this radio
represents; selected is the currently selected value from the model.
The radio is checked when value == selected. Emits :select with the
radio's value.
Display
| DSL macro | Macro form | Description |
|---|---|---|
text | text(content) or text(id, content) | Static text display |
rich_text | rich_text(id, spans) | Styled text with per-span formatting |
rule | rule() or rule(id) | Horizontal or vertical divider |
progress_bar | progress_bar(id, range, value) | Progress indicator |
tooltip | tooltip(id, tip, opts) do child end | Popup tip on hover |
image | image(id, source, opts) | Raster image from file path |
svg | svg(id, source, opts) | Vector image from SVG file |
qr_code | qr_code(id, data, opts) | QR code from a data string |
markdown | markdown(content) or markdown(id, content) | Rendered markdown |
canvas | canvas(id, opts) do layers end | Drawing surface with named layers |
text supports auto-ID (text("Hello")) and explicit-ID
(text("greeting", "Hello")). Key props: size, color, font,
wrapping, shaping, align_x, align_y.
rich_text displays styled text with individually formatted spans.
Each span is a map with optional keys: text (content), size,
color, font, link (clickable URL), underline, strikethrough,
line_height, padding, and highlight (background with optional
border). Example:
rich_text("greeting", [
%{text: "Hello, ", size: 16},
%{text: "world", size: 16, color: "#3b82f6", underline: true},
%{text: "!", size: 16}
])tooltip wraps a child widget. The child is the anchor; tip is
the tooltip text. Props: position (:top, :bottom, :left,
:right, :follow_cursor), gap.
image renders a raster image. Two source modes:
- Path-based (preferred):
image("photo", "path/to/file.png"). The renderer loads the file directly. No wire transfer. - Handle-based:
image("photo", handle: "avatar"). References an in-memory image created viaCommand.create_image/2.
Key props: content_fit, filter_method, width, height,
opacity, rotation (degrees), border_radius, scale, crop
(%{x, y, width, height}), alt (accessible label).
In-memory image handles:
# Create from encoded PNG/JPEG bytes:
{model, Command.create_image("avatar", png_bytes)}
# Create from raw RGBA pixels:
{model, Command.create_image("avatar", 512, 512, rgba_pixels)}
# Reference in view:
image("display", handle: "avatar")
# Update pixels:
{model, Command.update_image("avatar", new_pixels)}
# Delete:
{model, Command.delete_image("avatar")}Handle-based images send the entire payload over the wire in a single message, which blocks all other protocol traffic for large images. Prefer path-based loading when the file exists on disk. A chunked streaming alternative for large dynamic images is planned.
canvas contains named layers of shapes. See the Canvas reference.
Table
Data table with typed columns, rows, sorting, and scrolling.
table "users",
columns: [
%{key: "name", label: "Name", sortable: true},
%{key: "email", label: "Email", width: {:fill_portion, 2}},
%{key: "role", label: "Role", align: "center", sortable: true}
],
rows: [
%{"name" => "Alice", "email" => "alice@example.com", "role" => "Admin"},
%{"name" => "Bob", "email" => "bob@example.com", "role" => "User"}
],
sort_by: model.sort_by,
sort_order: model.sort_orderColumn format
Maps with these keys:
| Key | Type | Required | Default | Purpose |
|---|---|---|---|---|
key | string | yes | n/a | Lookup key into row data |
label | string | yes | n/a | Header display text |
sortable | boolean | no | false | Enable sort on header click |
align | "left" / "center" / "right" | no | "left" | Text alignment |
width | Length | no | :fill | Column width |
Row format
Rows are string-keyed maps where keys match column key values.
String keys are required to avoid dynamic atom creation from external
data (databases, JSON APIs). Values are rendered as text via
to_string/1.
Table props
| Prop | Type | Default | Purpose |
|---|---|---|---|
columns | [column()] | n/a | Column definitions |
rows | [row()] | n/a | Data rows |
header | boolean | n/a | Show header row |
separator | boolean | n/a | Show line below header |
sort_by | string | n/a | Currently sorted column key |
sort_order | :asc / :desc | n/a | Sort direction |
width | Length | :fill | Table width |
padding | Padding | n/a | Table padding |
header_text_size | number | n/a | Header font size |
row_text_size | number | n/a | Body font size |
cell_spacing | number | n/a | Horizontal cell gap |
row_spacing | number | n/a | Vertical row gap |
separator_thickness | number | n/a | Separator line thickness |
separator_color | Color | n/a | Separator line colour |
a11y | map | n/a | Accessibility overrides |
Sorting
Set sortable: true on columns that support sorting. The table emits
:sort events when a sortable header is clicked:
def update(model, %WidgetEvent{type: :sort, id: "users", data: %{column: col}}) do
dir = if model.sort_by == col and model.sort_order == :asc, do: :desc, else: :asc
%{model | sort_by: col, sort_order: dir}
endThe table itself does not sort. It displays rows in the order you
provide. Sort in your model or use Plushie.Data.query/2.
Interaction wrappers
pointer_area
Wraps a single child and captures pointer events from mouse, touch,
and pen input. Use for right-click menus, hover detection, drag
tracking, scroll capture, and custom cursor styles. All events use the
unified pointer model: the pointer field (:mouse, :touch,
:pen) identifies the device, and modifiers carries the current
modifier key state for shift-click, ctrl-drag, and similar patterns.
| Prop | Type | Purpose |
|---|---|---|
cursor | cursor atom | Mouse cursor on hover |
on_press | atom / string | Left button press event tag |
on_release | atom / string | Left button release event tag |
on_right_press | boolean | Enable right button press |
on_right_release | boolean | Enable right button release |
on_middle_press | boolean | Enable middle button press |
on_middle_release | boolean | Enable middle button release |
on_double_click | boolean | Enable double-click |
on_enter | boolean | Enable cursor enter |
on_exit | boolean | Enable cursor exit |
on_move | boolean | Enable cursor move (coalescable) |
on_scroll | boolean | Enable scroll wheel (coalescable) |
event_rate | integer | Max events/sec for move and scroll |
a11y | map | Accessibility overrides |
Cursor values: :pointer, :grab, :grabbing, :crosshair, :text,
:move, :not_allowed, :progress, :wait, :help,
:resizing_horizontally, :resizing_vertically, and others.
Move and scroll events carry pointer (device type) and modifiers
(current modifier key state):
pointer_area "canvas-area",
on_move: true,
on_press: :area_press,
on_scroll: true,
cursor: :crosshair do
canvas "drawing", width: 400, height: 300 do
# ...
end
end
# Shift-click for multi-select
def update(model, %WidgetEvent{type: :press, id: "canvas-area",
data: %{pointer: :mouse, modifiers: %{shift: true}}}) do
add_to_selection(model)
end
# Ctrl-drag for panning
def update(model, %WidgetEvent{type: :move, id: "canvas-area",
data: %{x: x, y: y, modifiers: %{ctrl: true}}}) do
pan_canvas(model, x, y)
end
# Scroll with pointer type
def update(model, %WidgetEvent{type: :scroll, id: "canvas-area",
data: %{delta_y: dy, pointer: :mouse}}) do
zoom(model, dy)
endsensor
Wraps a single child and emits events when the child's size changes or when it enters/exits visibility. Useful for responsive layouts, lazy loading, and intersection observation.
| Prop | Type | Purpose |
|---|---|---|
delay | integer | Delay (ms) before emitting events |
anticipate | number | Distance (px) to anticipate visibility |
on_resize | atom / string | Event tag for resize events |
event_rate | integer | Max events/sec for resize |
a11y | map | Accessibility overrides |
Events: :resize with %{width: w, height: h} in data.
overlay
Positions the second child as a floating overlay relative to the first child (anchor). Exactly two children required.
| Prop | Type | Default | Purpose |
|---|---|---|---|
position | :below / :above / :left / :right | :below | Overlay position |
gap | number | 0 | Space between anchor and overlay |
offset_x | number | 0 | Horizontal offset after positioning |
offset_y | number | 0 | Vertical offset after positioning |
flip | boolean | false | Auto-flip when overlay overflows viewport |
align | :start / :center / :end | :center | Cross-axis alignment |
width | Length | n/a | Overlay container width |
a11y | map | n/a | Accessibility overrides |
The overlay renders above all other content at the positioned location. See the Composition Patterns reference for a popover menu example.
themer
Applies a different theme to its children. Single child, single prop:
themer "dark-section", theme: :dark do
container padding: 12 do
text("info", "This section uses the dark theme")
end
endCommon props
Most widgets support a subset of these cross-cutting props:
:style- visual appearance. Accepts a preset atom (e.g.:primary,:danger) or aStyleMap. See the Styling reference.:a11y- accessibility attributes. See the Accessibility reference.:width/:height- sizing. Accepts:fill,:shrink,{:fill_portion, n}, or a pixel number. See the Layout reference.:event_rate- max events per second for high-frequency events. Supported onslider,vertical_slider,pointer_area,sensor,canvas, andpane_grid.
Renderer-side state
Some widgets hold state in the renderer that persists across re-renders. If their ID changes, this state resets:
text_input- cursor position, selection, undo historytext_editor- cursor, selection, scroll position, undocombo_box- search text, open/closed statescrollable- scroll positionpane_grid- pane sizes and arrangement
These widgets require explicit string IDs. scrollable and pane_grid
produce compile-time errors if you forget the ID.
keyed_column vs column
Use column for static layouts. Use keyed_column when children are
dynamic (added, removed, reordered). It diffs by child ID instead of
position, preserving widget state across list changes. Same props as
column minus align_x, clip, and wrap.
Auto-ID vs explicit ID
Layout containers (column, row, stack, grid, keyed_column,
responsive) and some display widgets (text, markdown, rule,
space, progress_bar) support auto-generated IDs. Omit the ID and
one is generated from the call site.
All interactive and stateful widgets require explicit string IDs for event routing and state persistence. See the DSL reference for the full breakdown.
Animatable props
Numeric props support renderer-side transitions via transition(),
spring(), and loop(). Commonly animated: max_width, max_height,
opacity, translate_x, translate_y, scale. See the
Animation reference.
Prop value types
These prop types are used across multiple widgets. The full styling types (Color, Theme, StyleMap, Border, Shadow, Gradient) are in the Styling reference. Layout types (Length, Padding, Alignment) are in the Layout reference.
Font
Used by: text, rich_text, text_input, text_editor.
| Value | Meaning |
|---|---|
:default | System default proportional font |
:monospace | System monospace font |
"Family Name" | Specific font family (must be loaded via settings/0) |
The full font spec struct (Plushie.Type.Font) also supports weight:
(:thin through :black), style: (:normal, :italic, :oblique),
and stretch: (:ultra_condensed through :ultra_expanded).
Shaping
Used by: text, rich_text, text_input, text_editor.
| Value | Meaning |
|---|---|
:basic | Simple left-to-right shaping (fastest) |
:advanced | Full Unicode shaping (ligatures, RTL, complex scripts) |
:auto | Let the renderer decide based on content |
Wrapping
Used by: text, rich_text.
| Value | Meaning |
|---|---|
:none | No wrapping (text overflows) |
:word | Break at word boundaries |
:glyph | Break at any character |
:word_or_glyph | Try word boundaries first, fall back to glyph |
Content fit
Used by: image, svg.
| Value | Meaning |
|---|---|
:contain | Scale to fit within bounds, preserving aspect ratio |
:cover | Scale to fill bounds, cropping if needed |
:fill | Stretch to fill bounds exactly (may distort) |
:none | No scaling (original size) |
:scale_down | Like :contain but never scales up |
Filter method
Used by: image.
| Value | Meaning |
|---|---|
:nearest | Pixel-perfect interpolation (blocky, good for pixel art) |
:linear | Smooth interpolation (good for photos) |
Tooltip position
Used by: tooltip.
| Value | Meaning |
|---|---|
:top | Above the widget |
:bottom | Below the widget |
:left | Left of the widget |
:right | Right of the widget |
:follow_cursor | Follows the mouse cursor |
Scroll direction
Used by: scrollable.
| Value | Meaning |
|---|---|
:vertical | Vertical scrolling (default) |
:horizontal | Horizontal scrolling |
:both | Bidirectional scrolling |
Scroll anchor
Used by: scrollable.
| Value | Meaning |
|---|---|
:start | Anchor at the top/left (default) |
:end | Anchor at the bottom/right |
See also
- Layout reference - sizing, alignment, and all layout containers with full prop tables
- Styling reference - Color, Theme, StyleMap, Border, Shadow, Gradient
- Canvas reference - shapes, layers, interactive groups
- Accessibility reference - the
a11yprop, roles, and keyboard navigation - Events reference - all event types delivered by widgets
- DSL reference - three forms, auto-IDs, compile-time validation
- Animation reference - transition, spring, loop descriptors
- Layout guide - layout applied to the pad
- Styling guide - themes and visual customization