# Built-in Widgets
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](windows-and-layout.md).
### 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. Supports
`circular_handle: true` for a round handle, with `handle_radius`
(float) controlling the circle's radius.
**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:
```elixir
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 via `Command.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:**
```elixir
# Create from encoded PNG/JPEG bytes:
{model, Command.create_image("avatar", png_bytes)}
# Create from raw RGBA pixels:
{model, Command.create_image_rgba("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](canvas.md).
## Table
`Plushie.Widget.Table`
**table** displays structured data in rows and columns with sortable
headers, row selection highlighting, and optional striped backgrounds.
Rows are real tree children, so adding, removing, or reordering rows
produces minimal wire patches (LIS-based diffing) instead of
re-sending the entire dataset.
The table ID is optional. Pass one when you need to match sort or
row_click events:
```elixir
table "users", columns: cols, selected: Selection.to_list(sel) do
for user <- model.users do
table_row user.id do
cell "name", text(user.name)
cell "email", text(user.email)
cell "actions", button("del-#{user.id}", "Delete")
end
end
end
```
For simple text-only tables, the `rows:` shorthand avoids the
do-block entirely:
```elixir
table columns: cols, rows: model.users
```
Both forms are mutually exclusive. The `rows:` shorthand expands
to `table_row`/`table_cell` children during build.
### Columns
Column definitions are maps passed via the `columns:` prop. Every
column needs a `key` (matching the row data field) and a `label`
(header text):
```elixir
columns: [
%{key: :name, label: "Name", sortable: true, width: :fill},
%{key: :email, label: "Email"},
%{key: :role, label: "Role", align: "center", width: 120}
]
```
| Key | Type | Default | Description |
|---|---|---|---|
| `key` | atom or string | required | Row data lookup key |
| `label` | string | required | Header display text |
| `sortable` | boolean | `false` | Header clickable for sort |
| `width` | Length | `:fill` | Column width |
| `align` | `"left"` `"center"` `"right"` | `"left"` | Cell alignment |
All column keys must be the same type (all atoms or all strings).
### Rich cells
Inside a `table_row`, each `cell` maps to a column by key. Cells
can contain any widget, not just text:
```elixir
table_row user.id do
cell "name", text(user.name, font: Font.new() |> Font.weight(:bold))
cell "status", progress_bar("prog", {0, 100}, user.progress)
cell "actions" do
button("edit-#{user.id}", "Edit")
button("del-#{user.id}", "Delete")
end
end
```
### Sorting
Mark columns as `sortable: true`. Clicking a sortable header emits
a `:sort` event with the column key as the value. The table displays
the sort indicator but does not reorder rows. Sort in your model:
```elixir
def update(model, %WidgetEvent{type: {:table, :sort}, id: "users", value: col}) do
dir = if model.sort_by == col and model.sort_order == :asc, do: :desc, else: :asc
sorted = Enum.sort_by(model.users, & &1[col], if(dir == :asc, do: :asc, else: :desc))
%{model | users: sorted, sort_by: col, sort_order: dir}
end
```
### Selection
Selection is app-managed. Pass selected row IDs via the `selected:`
prop; the renderer highlights those rows. Handle `:row_click` events
to update selection state using `Plushie.Selection`:
```elixir
def update(model, %WidgetEvent{type: {:table, :row_click}, id: row_id}) do
%{model | selection: Selection.toggle(model.selection, row_id)}
end
def view(model) do
table "users",
columns: cols,
selected: Selection.to_list(model.selection),
striped: true do
...
end
end
```
### Props
| Prop | Type | Default | Description |
|---|---|---|---|
| `columns` | `[map]` | | Column definitions (see above) |
| `rows` | `[map]` | | Data shorthand: text-only rows |
| `header` | boolean | `true` | Show header row |
| `selected` | `[string]` | | Row IDs to highlight |
| `striped` | boolean | `false` | Alternate row backgrounds |
| `separator` | float | `1.0` | Divider thickness (0.0 to hide) |
| `separator_color` | Color | | Divider colour |
| `sort_by` | string | | Currently sorted column key |
| `sort_order` | `:asc`/`:desc` | | Sort direction |
| `width` | Length | `:fill` | Table width |
| `height` | Length | | Table height (scrollable when set) |
| `padding` | Padding | | Cell internal padding |
| `header_text_size` | number | | Header font size |
| `row_text_size` | number | | Body font size (data shorthand) |
## Pane grid
`Plushie.Widget.PaneGrid`
Resizable tiled pane layout. Children are keyed by their node ID and
rendered as individual panes. The renderer manages internal pane sizes
and arrangement, persisted across re-renders by the widget's ID.
```elixir
pane_grid "editor", panes: ["left", "right"], spacing: 2 do
text_editor "left", model.left_source
text_editor "right", model.right_source
end
```
### Pane grid props
| Prop | Type | Default | Purpose |
|---|---|---|---|
| `panes` | `[string]` | *n/a* | List of pane identifiers (atoms coerced to strings) |
| `spacing` | number | `2` | Space between panes in pixels |
| `width` | Length | `:fill` | Grid width |
| `height` | Length | `:fill` | Grid height |
| `min_size` | number | `10` | Minimum pane size in pixels |
| `divider_color` | Color | *n/a* | Color for the split divider |
| `divider_width` | number | *n/a* | Divider thickness in pixels |
| `leeway` | number | *n/a* | Grabbable area around dividers in pixels |
| `split_axis` | `:horizontal` / `:vertical` | *n/a* | Controls the initial split direction |
### Pane grid events
| Event | Data | Description |
|---|---|---|
| `:pane_clicked` | *n/a* | Emitted when a pane is selected |
| `:pane_resized` | `%{split: string, ratio: float}` | Emitted when a split divider is moved |
| `:pane_dragged` | *n/a* | Emitted during pane drag operations |
| `:pane_focus_cycle` | *n/a* | Emitted on F6/Shift+F6 focus cycling |
### Usage patterns
Pane identifiers in the `panes` list determine which children map to
which pane. Each child's ID must match a pane identifier. Atom pane
identifiers are automatically coerced to strings.
The pane grid holds renderer-side state (pane sizes and arrangement).
If the widget's ID changes, this state resets. An explicit string ID
is required (compile-time error if omitted).
For accessibility, wrap the pane grid in a container with an explicit
role and label:
```elixir
container "editor-panes" do
a11y do
role :group
label "Editor panes"
end
pane_grid "grid", panes: ["left", "right"] do
text_editor "left", model.left
text_editor "right", model.right
end
end
```
## 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):
```elixir
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)
end
```
### sensor
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](composition-patterns.md#popover)
reference for a popover menu example.
### themer
Applies a different theme to its children. Single child, single prop:
```elixir
themer "dark-section", theme: :dark do
container padding: 12 do
text("info", "This section uses the dark theme")
end
end
```
## Common props
Most widgets support a subset of these cross-cutting props:
- **`:style`** - visual appearance. Accepts a preset atom (e.g.
`:primary`, `:danger`) or a `StyleMap`. See the
[Styling reference](themes-and-styling.md).
- **`:a11y`** - accessibility attributes. See the
[Accessibility reference](accessibility.md).
- **`:width` / `:height`** - sizing. Accepts `:fill`, `:shrink`,
`{:fill_portion, n}`, or a pixel number. See the
[Layout reference](windows-and-layout.md).
- **`:event_rate`** - max events per second for high-frequency events.
Supported on `slider`, `vertical_slider`, `pointer_area`, `sensor`,
`canvas`, and `pane_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 history
- **`text_editor`** - cursor, selection, scroll position, undo
- **`combo_box`** - search text, open/closed state
- **`scrollable`** - scroll position
- **`pane_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](dsl.md#auto-ids) 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](animation.md).
## 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](themes-and-styling.md). Layout types (Length, Padding,
Alignment) are in the [Layout reference](windows-and-layout.md).
### 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 |
### Auto scroll
Used by: `scrollable`.
When `auto_scroll: true` is set, the scrollable automatically scrolls to
reveal new content appended at the anchor end. This is useful for chat
logs, terminal output, and other append-only content where the user
expects to see the latest entries without manual scrolling.
```elixir
scrollable "log", direction: :vertical, anchor: :end, auto_scroll: true do
column spacing: 4 do
for entry <- model.log_entries do
text(entry.id, entry.text)
end
end
end
```
When the user manually scrolls away from the anchor, auto-scroll pauses
to avoid fighting the user's position. It resumes when the user scrolls
back to the anchor end.
## See also
- [Layout reference](windows-and-layout.md) - sizing, alignment, and all layout
containers with full prop tables
- [Styling reference](themes-and-styling.md) - Color, Theme, StyleMap, Border,
Shadow, Gradient
- [Canvas reference](canvas.md) - shapes, layers, interactive elements
- [Accessibility reference](accessibility.md) - the `a11y` prop, roles,
and keyboard navigation
- [Events reference](events.md) - all event types delivered by widgets
- [DSL reference](dsl.md) - three forms, auto-IDs, compile-time
validation
- [Animation reference](animation.md) - transition, spring, loop
descriptors
- [Layout guide](../guides/07-layout.md) - layout applied to the pad
- [Styling guide](../guides/08-styling.md) - themes and visual
customization