Components

Surfaces

PhoenixPaper.Typography, Divider, the pane layouts (ListDetail and SupportingPane), BottomSheet and SideSheet. MD3's base container is the Card (on the Data Display page); for any other panel, use the surface-container color roles and the rounded-pp-* corners directly.

Typography

MD3's 15 type roles (display, headline, title, body, label in large/medium/small) plus code. variant picks the style and a sensible tag; tag overrides the element independently (an h2 that looks like title-large). emphasized is M3 Expressive's bolder variant of each role, and color sets a text color role.

The type scale

display-large

display-medium

display-small

headline-large

headline-medium

headline-small

title-large
title-medium
title-small

body-large

body-medium

body-small

label-large label-medium label-small

emphasized

headline-small emphasized

title-medium emphasized

color

color="primary"

color="secondary"

color="tertiary"

color="error"

color="on-surface"

color="on-surface-variant"

Option Type Default Description
color "primary" | "secondary" | "tertiary" | "error" | "on-surface" | "on-surface-variant" nil text color role; unset inherits
emphasized boolean false M3 Expressive emphasized (heavier) style
paperize boolean true
tag "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "span" | "div" | "label" | "legend" | "figcaption" | "code" | "strong" | "em" | "small" nil overrides the tag the variant picks
variant "display-large" | "display-medium" | "display-small" | "headline-large" | "headline-medium" | "headline-small" | "title-large" | "title-medium" | "title-small" | "body-large" | "body-medium" | "body-small" | "label-large" | "label-medium" | "label-small" | "code" "body-large"
Slot Attrs Description
:inner_block

required

Show code
<.pp_typography variant="display-large">Display large</.pp_typography>
<.pp_typography variant="headline-medium">Headline medium</.pp_typography>
<.pp_typography variant="title-large">Title large</.pp_typography>
<.pp_typography variant="body-large">Body large</.pp_typography>
<.pp_typography variant="label-small">Label small</.pp_typography>
<.pp_typography variant="code">mix phx.new my_app</.pp_typography>

<%!-- tag independent of variant; Expressive emphasis; a color role --%>
<.pp_typography variant="title-large" tag="h2" emphasized>Section</.pp_typography>
<.pp_typography color="on-surface-variant">Muted text</.pp_typography>

Divider

A thin outline-variant line that groups content in lists and layouts. inset indents it from the leading edge, the way MD3 separates list items that have a leading icon.

Full width and inset

Above
Between
Below
Option Type Default Description
inset boolean false indent past a leading icon/avatar column instead of spanning full width
paperize boolean true
Show code
<.pp_divider />
<.pp_divider inset />

Pane Layout

New

MD3's canonical layouts, from its Layout foundations: window size classes (compact under 600dp, medium to 839dp, expanded from 840dp), 16/24dp margins, a 24dp spacer and 360dp fixed panes. pp_list_detail shows both panes on expanded windows and one at a time below, chosen with show_detail (going back is yours to render). pp_supporting_pane puts a 360dp supporting pane beside the main one, or below it on smaller windows. The panes have no surface of their own: put a Card or List inside.

pp_list_detail (narrow the window to see one pane at a time)

AL Ada Lovelace Notes on the engine
GH Grace Hopper Found a bug
AT Alan Turing Can machines think?

pp_supporting_pane

The main pane
Flexible: it takes whatever the supporting pane leaves.
Supporting
360dp beside the main pane from 840dp, below it before that.
Option Type Default Description
pp_list_detail paperize boolean true
pp_list_detail show_detail boolean false compact and medium windows show the detail pane instead of the list
pp_supporting_pane paperize boolean true
Slot Attrs Description
pp_list_detail :detail

required

the detail pane
pp_list_detail :list

required

the list pane
pp_supporting_pane :inner_block

required

the main (flexible) pane
pp_supporting_pane :supporting

required

the supporting pane
Show code
<.pp_list_detail show_detail={@message != nil}>
  <:list>
    <.pp_list>
      <.pp_list_item :for={m <- @messages} patch={~p"/inbox/#{m.id}"} active={m.id == @message_id}>
        {m.subject}
      </.pp_list_item>
    </.pp_list>
  </:list>
  <:detail>
    <.pp_card :if={@message} variant="filled">
      <:title>{@message.subject}</:title>
      {@message.body}
      <:actions>
        <%!-- Below 840dp only one pane shows: going back is yours --%>
        <.pp_button variant="text" patch={~p"/inbox"}>Back</.pp_button>
      </:actions>
    </.pp_card>
  </:detail>
</.pp_list_detail>

<.pp_supporting_pane>
  <article>...</article>
  <:supporting>
    <.pp_list>
      <.pp_list_item :for={r <- @related} navigate={r.path}>{r.title}</.pp_list_item>
    </.pp_list>
  </:supporting>
</.pp_supporting_pane>

Bottom Sheet

MD3's bottom sheet. modal (default) slides up over a scrim with a drag handle, built like Dialog: always in the DOM, show/2 and hide/2 JS commands, focus trapping, Escape or a scrim click to dismiss. standard renders in place for persistent content docked to the bottom. With the JS hook it can be dragged down to dismiss.

Try it

Option Type Default Description
drag_handle boolean true
id string (required)
label string nil aria-label of the sheet
on_cancel JS %JS{}
paperize boolean true
show boolean false modal: shown when first mounted
variant "modal" | "standard" "modal"
Slot Attrs Description
:inner_block

required

Show code
<.pp_button phx-click={PhoenixPaper.BottomSheet.show("share")}>Share</.pp_button>

<.pp_bottom_sheet id="share" label="Share">
  <.pp_list>
    <.pp_list_item phx-click="copy_link">
      <:leading><.pp_icon name="hero-link" /></:leading>
      Copy link
    </.pp_list_item>
  </.pp_list>
</.pp_bottom_sheet>

Side Sheet

MD3's side sheet at the end edge, for supplementary content and actions (filters, details). modal (default) slides in over a scrim with a header (optional back button, :title, close button) and :actions behind a divider; same show/hide JS and focus trapping as Dialog. standard renders in place as an aside beside your content.

Try it

Option Type Default Description
close_label string "Close"
id string (required)
on_back any nil a JS/event for a leading back button
on_cancel JS %JS{}
on_close any nil standard sheets: a JS/event for a close button
paperize boolean true
show boolean false modal: shown when first mounted
variant "modal" | "standard" "modal"
Slot Attrs Description
:actions
:inner_block

required

:title
Show code
<.pp_button phx-click={PhoenixPaper.SideSheet.show("filters")}>Filters</.pp_button>

<.pp_side_sheet id="filters">
  <:title>Filters</:title>
  <.pp_checkbox name="unread" label="Unread only" />
  <:actions>
    <.pp_button phx-click="apply_filters">Apply</.pp_button>
    <.pp_button variant="outlined" phx-click={PhoenixPaper.SideSheet.hide("filters")}>Cancel</.pp_button>
  </:actions>
</.pp_side_sheet>

z7ealth 2026