Components

Feedback

PhoenixPaper.Dialog, Progress, LoadingIndicator, Snackbar and Flash. MD3 has no alert banner or skeleton: reach for a snackbar or dialog, and a progress or loading indicator while content loads.

Dialog

MD3's dialog: 28dp corners on surface-container-high, a headline, supporting text and actions, with an optional hero icon. Always in the DOM and shown/hidden with the PhoenixPaper.Dialog.show/1 and hide/1 JS commands (like a generated core_components modal); focus is trapped, and Escape or a scrim click dismisses. variant="fullscreen" takes the whole screen; "responsive" is fullscreen on phones only.

basic with a hero icon, and fullscreen

Option Type Default Description
close_label string "Close" full-screen close button label
icon string nil a hero-* icon above the title (basic dialogs)
id string (required)
on_cancel JS %JS{}
paperize boolean true
show boolean false shown immediately when this element first mounts
variant "basic" | "fullscreen" | "responsive" "basic"
Slot Attrs Description
:actions
:inner_block

required

:title
Show code
<.pp_button phx-click={PhoenixPaper.Dialog.show("confirm-delete")}>Delete</.pp_button>

<.pp_dialog id="confirm-delete" icon="hero-trash">
  <:title>Delete this item?</:title>
  This can't be undone.
  <:actions>
    <.pp_button variant="text" phx-click={PhoenixPaper.Dialog.hide("confirm-delete")}>Cancel</.pp_button>
    <.pp_button variant="text" phx-click={JS.push("delete") |> PhoenixPaper.Dialog.hide("confirm-delete")}>
      Delete
    </.pp_button>
  </:actions>
</.pp_dialog>

<%!-- Full screen on every screen, or only on phones --%>
<.pp_dialog id="new-event" variant="responsive">...</.pp_dialog>

Progress

MD3 progress indicators, linear or circular (an SVG arc), determinate with a value or indeterminate without one. M3 Expressive adds a wavy track, a thick (8dp) variant, and the stop indicator at the track's end.

Linear: determinate, indeterminate, wavy, thick

Circular

Option Type Default Description
color "primary" | "secondary" | "tertiary" | "error" "primary"
label string nil aria-label
paperize boolean true
size integer 40 circular only — diameter in pixels
stop_indicator boolean true determinate linear only
thickness 4 | 8 4
value integer nil 0-100, nil for indeterminate
variant "linear" | "circular" "linear"
wavy boolean false M3 Expressive wavy active indicator
Show code
<.pp_progress value={72} label="Uploading" />
<.pp_progress label="Loading" />
<.pp_progress value={60} wavy />
<.pp_progress value={45} thickness={8} />

<.pp_progress variant="circular" value={65} color="tertiary" />
<.pp_progress variant="circular" />

Loading Indicator

M3 Expressive's loading indicator: a shape that morphs through MD3's shape library as it spins, for waits too short for a progress bar (pull to refresh, a card loading). contained sets it on a primary-container circle for indicators over content. Animated in CSS; Safari shows a rotating soft burst unless the JS hook is on.

Plain, contained, colors

Option Type Default Description
color "primary" | "secondary" | "tertiary" "primary"
contained boolean false
label string "Loading" aria-label
paperize boolean true
size integer 48 px
Show code
<.pp_loading_indicator label="Loading messages" />
<.pp_loading_indicator contained />
<.pp_loading_indicator color="tertiary" size={36} />

Snackbar

A brief message on MD3's inverse-surface, always that color, with an optional :action and a close button (on_close). two_line fits a longer message. Dismissal is yours (a Process.send_after/3 clearing the open assign, like generated flash messages), or client-side with on_close + auto_hide_duration. It's fixed at the bottom of the viewport (centered on phones, bottom-start from sm) and enters with MD3's fade-and-expand; positioned={false} drops the fixed placement so you can place it yourself, as these demos do. For Phoenix flash messages, use Flash below.

Try it

Changes saved
Link copied
Your draft was saved on this device. Sign in to sync it everywhere.
Option Type Default Description
auto_hide_duration integer nil milliseconds after which the snackbar triggers on_close itself (client-side; needs on_close set)
on_close JS nil when set, renders MD3's trailing close icon button, running this
open boolean true
paperize boolean true
positioned boolean true keep the viewport-anchored `fixed` positioning — set false to drop it and place the chip yourself (e.g. inside PhoenixPaper.Flash's stack)
two_line boolean false message above the action (MD3 longer action)
Slot Attrs Description
:action
:inner_block

required

Show code
<.pp_snackbar open={@message != nil}>
  {@message}
  <:action>
    <.pp_button variant="text" phx-click="undo">Undo</.pp_button>
  </:action>
</.pp_snackbar>

<%!-- on_close renders a trailing close button; pair it with auto_hide_duration
      for a hook-free client-side auto-dismiss --%>
<.pp_snackbar on_close={JS.push("dismiss")} auto_hide_duration={5000}>
  Link copied
</.pp_snackbar>

<%!-- two_line: a longer message above its action --%>
<.pp_snackbar two_line>
  Your draft was saved on this device. Sign in to sync it everywhere.
  <:action><.pp_button variant="text" phx-click="sign_in">Sign in</.pp_button></:action>
</.pp_snackbar>

Flash

Phoenix's @flash rendered as stacked snackbars: the Material counterpart of a generated core_components' flash_group. Drop pp_flash_group once in the root layout. Dismiss is wired to LiveView's built-in lv:clear-flash (no handler in your LiveView); auto_hide_duration is opt-in. Monochrome and text-only by spec: every kind looks the same, an inverse-surface snackbar.

Trigger a real flash

This whole page renders a pp_flash_group bound to @flash instead of the default flash_group. The buttons above call put_flash/3; a real chip slides in at the top-right, auto-hides after 6s, or dismiss it with the ✕ (LiveView's built-in lv:clear-flash, no handler). It also sets connection_notices: stop the server and a "We can't find the internet" chip appears until the socket reconnects.

Appearance (inline, static)

The same chips shown inline (each is a pp_snackbar positioned=false) rather than fixed to the viewport corner:

Workbook saved.
Could not reach the guest agent.
Option Type Default Description
pp_flash_group auto_hide_duration integer nil milliseconds after which each chip clears itself via lv:clear-flash (opt-in)
pp_flash_group client_error_title string "We can't find the internet"
pp_flash_group connection_notices boolean false also render the hidden client/server connection-lost chips (see the module doc)
pp_flash_group flash map (required) the @flash map from the assigns
pp_flash_group kinds list [:info, :error] flash keys to render, in stacking order
pp_flash_group paperize boolean true
pp_flash_group reconnecting_text string "Attempting to reconnect"
pp_flash_group server_error_title string "Something went wrong!"
pp_flash auto_hide_duration integer nil
pp_flash flash map (required)
pp_flash kind atom (required)
pp_flash paperize boolean true
Show code
<%!-- Drop pp_flash_group once, where a generated <.flash_group> goes.
      auto_hide_duration is opt-in; dismissal needs no handler (lv:clear-flash).
      connection_notices adds the generated flash_group's connection-lost chips. --%>
<.pp_flash_group
  flash={@flash}
  auto_hide_duration={6000}
  connection_notices
  client_error_title={gettext("We can't find the internet")}
/>

# In the LiveView: an ordinary put_flash/3 shows a chip
def handle_event("flash_info", _params, socket),
  do: {:noreply, put_flash(socket, :info, "Workbook saved.")}

def handle_event("flash_error", _params, socket),
  do: {:noreply, put_flash(socket, :error, "Could not reach the guest agent.")}