Sheet component — a sliding side panel that enters from any edge of the screen.
A sheet is a modal panel that slides in from a screen edge, covering a portion of the page. It is ideal for secondary workflows that don't need a full page — edit forms, settings panels, filter builders, detail views, and navigation drawers in mobile layouts.
Unlike Drawer, Sheet has a richer semantic sub-component set including
sheet_title/1 and sheet_description/1 for proper ARIA labelling, and a
size system (sm/md/lg/xl/full) to control the panel's dimension.
Reuses the PhiaDialog JavaScript hook — no additional hook registration
beyond what Dialog already requires.
Hook Registration
Copy the hook via mix phia.add sheet (or mix phia.add dialog), then
register in app.js:
# assets/js/app.js
import PhiaDialog from "./hooks/dialog"
let liveSocket = new LiveSocket("/live", Socket, {
hooks: { PhiaDialog }
})Sub-components
| Function | Purpose |
|---|---|
sheet/1 | Root container — hook mount point |
sheet_header/1 | Title + description layout container |
sheet_title/1 | <h2> heading — set :id for ARIA linkage |
sheet_description/1 | <p> supporting text — set :id for ARIA |
sheet_footer/1 | Action row at the bottom (save, cancel) |
sheet_close/1 | × close button in the top-right corner |
Sides
| Value | Slides from | Panel sizing |
|---|---|---|
"right" | right edge | Full height, configurable width (default) |
"left" | left edge | Full height, configurable width |
"top" | top edge | Full width, configurable height |
"bottom" | bottom edge | Full width, configurable height |
Sizes
| Value | Width (right/left) | Height (top/bottom) | Best For |
|---|---|---|---|
"sm" | w-64 (256px) | h-64 | Simple notifications, tips |
"md" | w-96 (384px) | h-96 (default) | Short forms, quick edits |
"lg" | max-w-lg | max-h-64 | Medium forms, settings |
"xl" | max-w-xl | max-h-96 | Complex forms, rich content |
"full" | max-w-full | max-h-full | Full-width panels, mobile |
Example — Edit Record Form (right side)
The most common use case: a form to edit a record without leaving the page.
<.sheet id="edit-user-sheet" open={@show_edit} side="right" size="lg">
<.sheet_header>
<.sheet_title id="edit-user-sheet-title">
Edit User
</.sheet_title>
<.sheet_description id="edit-user-sheet-description">
Update the user's profile information below.
</.sheet_description>
</.sheet_header>
<div class="p-6">
<.form for={@changeset} phx-submit="save_user" phx-change="validate_user">
<.input name="name" label="Full Name" value={@changeset.data.name} />
<.input name="email" label="Email" value={@changeset.data.email} />
<.input name="role" label="Role" value={@changeset.data.role} />
</.form>
</div>
<.sheet_footer>
<.sheet_close />
<button phx-click="cancel_edit" class="...">Cancel</button>
<button phx-click="save_user" class="...">Save changes</button>
</.sheet_footer>
<.sheet_close />
</.sheet>Example — Filter Panel (left side)
A filter sidebar that slides in from the left:
<.sheet id="filter-sheet" open={@show_filters} side="left" size="md">
<.sheet_header>
<.sheet_title id="filter-sheet-title">Filters</.sheet_title>
</.sheet_header>
<div class="p-6 space-y-4">
<.select name="status" label="Status" options={@status_options} />
<.select name="category" label="Category" options={@category_options} />
<.input type="date" name="from" label="From date" />
<.input type="date" name="to" label="To date" />
</div>
<.sheet_footer>
<button phx-click="clear_filters">Clear all</button>
<button phx-click="apply_filters">Apply</button>
</.sheet_footer>
<.sheet_close />
</.sheet>Example — Mobile Navigation Drawer (bottom side)
A mobile-friendly navigation menu that slides up from the bottom:
<.sheet id="mobile-nav" open={@show_mobile_nav} side="bottom" size="full">
<.sheet_header>
<.sheet_title id="mobile-nav-title">Navigation</.sheet_title>
</.sheet_header>
<nav class="p-4 grid grid-cols-2 gap-2">
<.link navigate="~p"/dashboard"" phx-click="close_mobile_nav" class="...">
Dashboard
</.link>
<.link navigate={~p"/reports"} phx-click="close_mobile_nav" class="...">
Reports
</.link>
</nav>
<.sheet_close />
</.sheet>Controlling Visibility
Control the sheet via the :open assign. Toggle from event handlers:
# Open the sheet
def handle_event("edit_user", %{"id" => id}, socket) do
user = Accounts.get_user!(id)
{:noreply, socket |> assign(:editing_user, user) |> assign(:show_edit, true)}
end
# Close the sheet (cancel or save)
def handle_event("cancel_edit", _params, socket) do
{:noreply, assign(socket, :show_edit, false)}
endARIA
Always set aria-labelledby pointing to sheet_title/1's :id. This is
automatically handled if you use the convention id="{sheet-id}-title".
The sheet/1 container already includes aria-labelledby="{id}-title".
Accessibility
role="dialog"andaria-modal="true"on the sliding panelaria-labelledbyon the panel references the title for screen reader context- Focus trap: Tab/Shift+Tab stay within the open sheet
- Escape key closes the sheet and returns focus to the triggering element
- Backdrop click closes the sheet when
on_closeis wired up - The × close button has
aria-label="Close sheet"and is always in the Tab order for keyboard users
Summary
Functions
Root Sheet container. Binds the PhiaDialog JS hook for focus trap and
keyboard handling (Escape to close, Tab to cycle focus within the panel).
The × close button rendered in the top-right corner of the sheet panel.
Supporting text below the sheet title.
Action row at the bottom of the sheet.
Layout container for the sheet title and optional description.
Sheet heading rendered as <h2>.
Functions
Root Sheet container. Binds the PhiaDialog JS hook for focus trap and
keyboard handling (Escape to close, Tab to cycle focus within the panel).
The open boolean controls visibility via the hidden CSS class on the
outer container. The side attribute controls which edge the panel slides
from. The size attribute controls the panel's cross-axis dimension.
The on_close attr wires a Phoenix.LiveView.JS command to the backdrop
click — typically used to update the visibility assign:
<.sheet id="settings" open={@show_settings}
on_close={JS.push("close_settings")}>Or using phx-click directly:
<.sheet id="settings" open={@show_settings}
on_close={JS.push("close_settings") |> JS.hide(to: "#settings")}>Attributes
id(:string) (required) - Unique sheet ID — thePhiaDialoghook mount point.open(:boolean) - Whether the sheet is currently visible. Toggle via LiveView assigns. Defaults tofalse.side(:string) - Screen edge the panel slides in from. Defaults to"right". Must be one of"top","right","bottom", or"left".size(:string) - Width for left/right sides, or height for top/bottom sides. Defaults to"md". Must be one of"sm","md","lg","xl", or"full".on_close(Phoenix.LiveView.JS) -Phoenix.LiveView.JScommand executed when the backdrop is clicked. Use to update your visibility assign:on_close={JS.push("close_sheet")}Defaults to
nil.safe_area(:boolean) - Whentrueandsideis"bottom", addspb-[env(safe-area-inset-bottom)]to the sheet panel for proper spacing on devices with a home indicator or notch (e.g. iPhone X+).Defaults to
false.class(:string) - Additional CSS classes for the sliding panel. Defaults tonil.Global attributes are accepted. Extra HTML attributes forwarded to the root element.
Slots
inner_block(required) - Sheet content:sheet_header/1, body div,sheet_footer/1,sheet_close/1.
The × close button rendered in the top-right corner of the sheet panel.
The PhiaDialog JS hook listens for clicks on data-sheet-close and:
- Adds the
hiddenclass to the root sheet element - Returns focus to the element that was focused before the sheet opened
This button is always visible (unlike the toast close button). Keyboard
users can Tab to it or press Escape to close.
Attributes
class(:string) - Additional CSS classes. Defaults tonil.- Global attributes are accepted. Extra HTML attributes.
Supporting text below the sheet title.
Use to provide context about what the sheet is for or what the user should
do. Set :id if you want to also wire aria-describedby on the sheet
container for richer screen reader support.
<.sheet id="settings" aria-describedby="settings-description">
<.sheet_description id="settings-description">
Changes take effect immediately and sync across all devices.
</.sheet_description>
</.sheet>Attributes
id(:string) - Element ID foraria-describedbylinkage from the sheet container. Defaults tonil.class(:string) - Additional CSS classes. Defaults tonil.- Global attributes are accepted. Extra HTML attributes.
Slots
inner_block(required) - Description text.
Layout container for the sheet title and optional description.
Provides consistent padding and vertical spacing. Always place this at the top of the sheet content, before the scrollable body area.
Attributes
class(:string) - Additional CSS classes. Defaults tonil.- Global attributes are accepted. Extra HTML attributes.
Slots
inner_block(required) -sheet_title/1and optionallysheet_description/1.
Sheet heading rendered as <h2>.
Set :id to "{sheet-id}-title" — the parent sheet/1 includes
aria-labelledby="{id}-title" by default. This ensures screen readers
announce the sheet's purpose when it opens.
<.sheet id="edit-profile" ...>
<.sheet_title id="edit-profile-title">Edit Profile</.sheet_title>
</.sheet>Attributes
id(:string) - Element ID for ARIA linkage. Set to"{sheet-id}-title"so the sheet's built-inaria-labelledbyresolves correctly.Defaults to
nil.class(:string) - Additional CSS classes. Defaults tonil.Global attributes are accepted. Extra HTML attributes.
Slots
inner_block(required) - Title text.