PhiaUi.Components.Sheet (phia_ui v0.1.17)

Copy Markdown View Source

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

FunctionPurpose
sheet/1Root container — hook mount point
sheet_header/1Title + 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/1Action row at the bottom (save, cancel)
sheet_close/1× close button in the top-right corner

Sides

ValueSlides fromPanel sizing
"right"right edgeFull height, configurable width (default)
"left"left edgeFull height, configurable width
"top"top edgeFull width, configurable height
"bottom"bottom edgeFull width, configurable height

Sizes

ValueWidth (right/left)Height (top/bottom)Best For
"sm"w-64 (256px)h-64Simple notifications, tips
"md"w-96 (384px)h-96 (default)Short forms, quick edits
"lg"max-w-lgmax-h-64Medium forms, settings
"xl"max-w-xlmax-h-96Complex forms, rich content
"full"max-w-fullmax-h-fullFull-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)}
end

ARIA

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" and aria-modal="true" on the sliding panel
  • aria-labelledby on 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_close is 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

sheet(assigns)

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 — the PhiaDialog hook mount point.

  • open (:boolean) - Whether the sheet is currently visible. Toggle via LiveView assigns. Defaults to false.

  • 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.JS command executed when the backdrop is clicked. Use to update your visibility assign:

    on_close={JS.push("close_sheet")}

    Defaults to nil.

  • safe_area (:boolean) - When true and side is "bottom", adds pb-[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 to nil.

  • Global attributes are accepted. Extra HTML attributes forwarded to the root element.

Slots

sheet_close(assigns)

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:

  1. Adds the hidden class to the root sheet element
  2. 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 to nil.
  • Global attributes are accepted. Extra HTML attributes.

sheet_description(assigns)

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 for aria-describedby linkage from the sheet container. Defaults to nil.
  • class (:string) - Additional CSS classes. Defaults to nil.
  • Global attributes are accepted. Extra HTML attributes.

Slots

  • inner_block (required) - Description text.

sheet_footer(assigns)

Action row at the bottom of the sheet.

Renders a flex row of buttons, right-aligned on desktop and stacked vertically (reversed) on mobile. Place the primary action button last in the template for natural desktop order.

<.sheet_footer>
  <button phx-click="cancel">Cancel</button>
  <button phx-click="save" class="...">Save changes</button>
</.sheet_footer>

Attributes

  • class (:string) - Additional CSS classes. Defaults to nil.
  • Global attributes are accepted. Extra HTML attributes.

Slots

  • inner_block (required) - Footer content — typically cancel + confirm buttons.

sheet_header(assigns)

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 to nil.
  • Global attributes are accepted. Extra HTML attributes.

Slots

sheet_title(assigns)

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-in aria-labelledby resolves correctly.

    Defaults to nil.

  • class (:string) - Additional CSS classes. Defaults to nil.

  • Global attributes are accepted. Extra HTML attributes.

Slots

  • inner_block (required) - Title text.