Components

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

Getting started

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>

z7ealth 2026