Actions
PhoenixPaper.Button, IconButton, SplitButton, ButtonGroup, Fab and FabMenu: the MD3 (and M3 Expressive) action components.
Button
MD3's five button styles (filled, tonal, elevated, outlined, text) in the Expressive sizes xs–xl and round or square shapes; pressing morphs the corners on the Expressive spring. href, navigate or patch renders a link instead of a button element. Toggle buttons are built in: selected={...} makes a controlled toggle (aria-pressed from your assigns plus phx-click), toggle flips it on the client, and group makes a client-side set exclusive.
variant
color
size (round), and square
Icons, loading, link
Toggle, controlled (any combination)
Toggle, client-side (toggle)
| Option | Type | Default | Description |
|---|---|---|---|
color
|
"primary" | "secondary" | "tertiary" | "error" | "inherit"
|
nil
|
the role the variant is built on; unset uses MD3's default |
disabled
|
boolean
|
false
|
|
group
|
string
|
nil
|
exclusive client-side toggle across buttons sharing this name (implies toggle) |
href
|
any
|
nil
|
renders an <a> (via Phoenix.Component.link/1) instead of a <button> |
loading
|
boolean
|
false
|
shows a spinner in place of start_icon and disables the button |
navigate
|
any
|
nil
|
like href, a LiveView live navigation |
on_toggle
|
JS
|
%JS{}
|
extra JS run after a client-side toggle, e.g. JS.push(...) |
paperize
|
boolean
|
true
|
apply PhoenixPaper's Material styling |
patch
|
any
|
nil
|
like href, a LiveView live patch |
position
|
"relative" | "fixed" | "absolute" | "sticky"
|
"relative"
|
the root's CSS position; set it here, not via class |
ripple
|
boolean
|
true
|
the ripple on click/tap — off whenever paperize is false, see PhoenixPaper.Ripple |
selected
|
boolean
|
nil
|
makes it a toggle button; true/false is the (initial) selected state |
shape
|
"round" | "square"
|
"round"
|
|
size
|
"xs" | "sm" | "md" | "lg" | "xl"
|
"sm"
|
|
toggle
|
boolean
|
false
|
flip selected on the client with JS commands, no round trip |
type
|
"button" | "submit" | "reset"
|
"button"
|
|
variant
|
"filled" | "tonal" | "elevated" | "outlined" | "text"
|
"filled"
|
| Slot | Attrs | Description |
|---|---|---|
:end_icon
|
|
an icon after the label |
:inner_block
required |
|
|
:start_icon
|
|
an icon before the label, replaced by the spinner when loading |
Show code
<.pp_button :for={variant <- ~w(filled tonal elevated outlined text)} variant={variant}>
{variant}
</.pp_button>
<.pp_button :for={size <- ~w(xs sm md lg xl)} size={size}>{size}</.pp_button>
<.pp_button shape="square" variant="tonal">square</.pp_button>
<.pp_button variant="outlined">
<:start_icon><.pp_icon name="hero-trash" size="sm" /></:start_icon>
Delete
</.pp_button>
<.pp_button loading>Saving</.pp_button>
<.pp_button variant="text" navigate={~p"/getting-started"}>Getting started</.pp_button>
<%!-- Controlled toggle: selected from your assigns, plus phx-click --%>
<.pp_button
variant="outlined"
selected={"bold" in @formats}
phx-click="toggle_format"
phx-value-format="bold"
>
Bold
</.pp_button>
<%!-- Client-side toggle: no assigns, selected is the initial state --%>
<.pp_button variant="tonal" toggle selected={false}>Italic</.pp_button>
# In the LiveView (controlled example):
def handle_event("toggle_format", %{"format" => format}, socket) do
formats = socket.assigns.formats
formats = if format in formats, do: MapSet.delete(formats, format), else: MapSet.put(formats, format)
{:noreply, assign(socket, :formats, formats)}
end
Icon Button
An icon-only button: standard (no container), filled, tonal or outlined, in the Expressive sizes, narrow/default/wide widths and round/square shapes. label is required: it's the accessible name and the native tooltip. Like Button it toggles (selected / toggle / group), and selected_icon swaps the glyph, MD3's outlined → filled pair.
variant
size and width
Toggle with selected_icon (click)
| Option | Type | Default | Description |
|---|---|---|---|
color
|
"primary" | "secondary" | "tertiary" | "error" | "inherit"
|
nil
|
the role the variant is built on; unset uses MD3's default |
disabled
|
boolean
|
false
|
|
group
|
string
|
nil
|
exclusive client-side toggle group name |
href
|
any
|
nil
|
renders a link instead of a button |
icon
|
string
|
nil
|
a hero-* icon name; or use the inner block |
label
|
string (required)
|
|
accessible name (aria-label) and title |
navigate
|
any
|
nil
|
|
on_toggle
|
JS
|
%JS{}
|
extra JS run after a client-side toggle |
paperize
|
boolean
|
true
|
|
patch
|
any
|
nil
|
|
position
|
"relative" | "fixed" | "absolute" | "sticky"
|
"relative"
|
the root's CSS position; set it here, not via class |
ripple
|
boolean
|
true
|
|
selected
|
boolean
|
nil
|
makes it a toggle; the (initial) selected state |
selected_icon
|
string
|
nil
|
icon shown while a toggle is selected (MD3's filled glyph) |
shape
|
"round" | "square"
|
"round"
|
|
size
|
"xs" | "sm" | "md" | "lg" | "xl"
|
"sm"
|
|
title
|
any
|
nil
|
native tooltip; defaults to label, false drops it |
toggle
|
boolean
|
false
|
flip selected on the client, no round trip |
type
|
"button" | "submit" | "reset"
|
"button"
|
|
variant
|
"standard" | "filled" | "tonal" | "outlined"
|
"standard"
|
|
width
|
"narrow" | "default" | "wide"
|
"default"
|
| Slot | Attrs | Description |
|---|---|---|
:inner_block
|
|
custom icon markup, instead of the icon attr |
Show code
<.pp_icon_button :for={variant <- ~w(standard filled tonal outlined)} variant={variant} icon="hero-heart" label={variant} />
<.pp_icon_button size="lg" variant="tonal" icon="hero-star" label="Star" />
<.pp_icon_button width="wide" variant="filled" icon="hero-plus" label="Add" />
<%!-- A toggle that swaps to the filled glyph when selected --%>
<.pp_icon_button icon="hero-star" selected_icon="hero-star-solid" label="Star" toggle selected={false} />
Split Button
A primary action joined to a trailing button that opens a menu of related actions (M3 Expressive). The halves sit 2dp apart; opening the menu morphs the trailing half into a circle and flips its chevron. phx-click and other global attrs go on the leading button; the menu holds pp_menu_item/1s.
Try it
| Option | Type | Default | Description |
|---|---|---|---|
anchor
|
"bottom-start" | "bottom-end" | "top-start" | "top-end"
|
"bottom-end"
|
|
color
|
"primary" | "secondary" | "tertiary" | "error"
|
nil
|
|
disabled
|
boolean
|
false
|
|
id
|
string (required)
|
|
|
menu_label
|
string
|
"More options"
|
|
paperize
|
boolean
|
true
|
|
ripple
|
boolean
|
true
|
|
size
|
"xs" | "sm" | "md" | "lg" | "xl"
|
"sm"
|
|
type
|
"button" | "submit" | "reset"
|
"button"
|
|
variant
|
"filled" | "tonal" | "elevated" | "outlined"
|
"filled"
|
| Slot | Attrs | Description |
|---|---|---|
:inner_block
required |
|
the leading button's label |
:menu
required |
|
the menu's pp_menu_item/1s |
:start_icon
|
|
Show code
<.pp_split_button id="send" phx-click="send">
Send
<:menu>
<.pp_menu_item icon="hero-clock" phx-click="schedule">Schedule send</.pp_menu_item>
<.pp_menu_item icon="hero-document" phx-click="save_draft">Save draft</.pp_menu_item>
</:menu>
</.pp_split_button>
Button Group
Groups related buttons (M3 Expressive). standard keeps them as separate buttons in a row; connected joins them into one segmented control with shared inner corners. Give the buttons a toggle group for a single-select control, like the exclusive view switch below.
standard
connected, controlled single-select (click)
connected, client-side single-select (group)
| Option | Type | Default | Description |
|---|---|---|---|
full_width
|
boolean
|
false
|
|
paperize
|
boolean
|
true
|
|
size
|
"xs" | "sm" | "md" | "lg" | "xl"
|
"sm"
|
|
variant
|
"standard" | "connected"
|
"standard"
|
| Slot | Attrs | Description |
|---|---|---|
:inner_block
required |
|
Show code
<.pp_button_group>
<.pp_button variant="tonal">Day</.pp_button>
<.pp_button variant="tonal">Week</.pp_button>
<.pp_button variant="tonal">Month</.pp_button>
</.pp_button_group>
<%!-- A single-select segmented control, client-side --%>
<.pp_button_group variant="connected">
<.pp_button variant="outlined" group="view" selected>Columns</.pp_button>
<.pp_button variant="outlined" group="view" selected={false}>Grid</.pp_button>
<.pp_button variant="outlined" group="view" selected={false}>Grouped</.pp_button>
</.pp_button_group>
Floating Action Button
The screen's primary action: an icon on a primary-container square (MD3 FABs are 16dp-corner squares, not circles), in default, medium and large sizes. icon and label are attrs: label is the accessible name, and shown beside the icon with extended. Anchor one to a corner with position="fixed" plus offsets in class.
size
color
extended, lowered
| Option | Type | Default | Description |
|---|---|---|---|
color
|
"primary-container" | "secondary-container" | "tertiary-container" | "primary" | "secondary" | "tertiary" | "surface"
|
"primary-container"
|
|
disabled
|
boolean
|
false
|
|
extended
|
boolean
|
false
|
show the label next to the icon |
icon
|
string
|
nil
|
a hero-* icon name; or use the inner block |
label
|
string (required)
|
|
accessible name; the visible text when extended |
lowered
|
boolean
|
false
|
level-1 resting shadow instead of level 3 |
paperize
|
boolean
|
true
|
|
position
|
"relative" | "fixed" | "absolute" | "sticky"
|
"relative"
|
the root's CSS position; set it here, not via class |
ripple
|
boolean
|
true
|
|
size
|
"default" | "medium" | "large"
|
"default"
|
|
type
|
"button" | "submit" | "reset"
|
"button"
|
| Slot | Attrs | Description |
|---|---|---|
:inner_block
|
|
custom icon markup, instead of the icon attr |
Show code
<.pp_fab icon="hero-pencil" label="Compose" />
<.pp_fab size="large" color="tertiary-container" icon="hero-plus" label="Add" />
<.pp_fab extended icon="hero-pencil" label="Compose" />
<%!-- Anchored to the screen corner --%>
<.pp_fab position="fixed" class="bottom-6 right-6" icon="hero-plus" label="Add" />
FAB Menu
A FAB that opens a short stack of related actions, M3 Expressive's replacement for the speed dial. Tapping it morphs the FAB into a round close button and the items rise above it as labeled pills; the close button, clicking outside, or Escape closes it. Pure CSS. Items link (href/navigate/patch) or fire on_click (a JS).
Try it
| Option | Type | Default | Description |
|---|---|---|---|
close_icon
|
string
|
"hero-x-mark"
|
the close button's icon |
color
|
"primary" | "secondary" | "tertiary"
|
"primary"
|
|
default_open
|
boolean
|
false
|
|
icon
|
string
|
"hero-plus"
|
the FAB's icon |
id
|
string (required)
|
|
|
label
|
string (required)
|
|
accessible name of the FAB |
paperize
|
boolean
|
true
|
|
position
|
"relative" | "fixed" | "absolute" | "sticky"
|
"relative"
|
the root's CSS position; offsets go in class |
ripple
|
boolean
|
true
|
|
size
|
"default" | "medium" | "large"
|
"default"
|
| Slot | Attrs | Description |
|---|---|---|
:item
required |
label, icon, href, navigate, patch, on_click
|
a menu action |
Show code
<.pp_fab_menu id="create" label="Create" position="fixed" class="bottom-4 right-4">
<:item icon="hero-document" label="Document" navigate={~p"/docs/new"} />
<:item icon="hero-table-cells" label="Spreadsheet" on_click={JS.push("new_sheet")} />
<:item icon="hero-photo" label="Photo" on_click={JS.push("upload")} />
</.pp_fab_menu>