PhiaUi.Components.Drawer (phia_ui v0.1.17)

Copy Markdown View Source

Drawer component — a sliding panel that enters from any edge of the screen.

A drawer provides access to supplementary content or actions without leaving the current page. Unlike Sheet, the Drawer is designed for content-heavy panels and uses its own dedicated PhiaDrawer JavaScript hook (rather than sharing PhiaDialog) with CSS transform animations for the enter/exit motion.

When to use Drawer vs Sheet

AspectDrawerSheet
JS HookPhiaDrawer (dedicated)PhiaDialog (shared)
Panel anatomySimple (header/footer/close)Rich (title/description/close)
Triggerdrawer_trigger/1 buttonControlled by :open assign
AnimationCSS transform slideCSS transform slide
Use caseSide nav, detail views, step formsEdit forms, filter panels

Use Drawer when you want a dedicated trigger button colocated with the content and a simpler, content-driven panel. Use Sheet when state from the server controls open/close or when you need the richer ARIA sub-components.

Hook Registration

Copy the hook via mix phia.add drawer, then register it in app.js:

# assets/js/app.js
import PhiaDrawer from "./hooks/drawer"

let liveSocket = new LiveSocket("/live", Socket, {
  hooks: { PhiaDrawer }
})

Sub-components

FunctionPurpose
drawer/1Root container — wraps trigger and content
drawer_trigger/1Button that opens the drawer
drawer_content/1Sliding panel + backdrop — the hook mount point
drawer_header/1Title and description layout container
drawer_footer/1Action row at the bottom
drawer_close/1× close button in the top-right corner

Directions

ValueSlides fromDefault panel dimensions
"bottom"bottom edgeFull width, max 85vh, rounded top corners
"top"top edgeFull width, max 85vh, rounded bottom corners
"left"left edge75% width, max sm, full height
"right"right edge75% width, max sm, full height

Example — Settings Drawer (right)

The canonical drawer pattern: a button triggers a right-side panel.

<.drawer id="settings-drawer">
  <.drawer_trigger drawer_id="settings-drawer">
    <.button>
      <.icon name="settings" class="mr-2" />
      Settings
    </.button>
  </.drawer_trigger>

  <.drawer_content id="settings-drawer-content" direction="right">
    <.drawer_header>
      <h2 id="settings-drawer-content-title" class="text-lg font-semibold">
        Settings
      </h2>
      <p class="text-sm text-muted-foreground">
        Manage your account preferences.
      </p>
    </.drawer_header>
    <.drawer_close />
    <div class="p-6 space-y-4">
      <.input name="display_name" label="Display name" value={@user.name} />
      <.select name="timezone" label="Timezone" options={@timezones} />
      <.switch name="email_notifications" label="Email notifications" checked={@user.email_notifs} />
    </div>
    <.drawer_footer>
      <button phx-click="cancel_settings">Cancel</button>
      <button phx-click="save_settings" class="...">Save</.button>
    </.drawer_footer>
  </.drawer_content>
</.drawer>

Example — Mobile Bottom Sheet

A bottom drawer is the mobile-native pattern for action sheets:

<.drawer id="actions-drawer">
  <.drawer_trigger drawer_id="actions-drawer" class="md:hidden">
    <.button variant="outline">Actions</.button>
  </.drawer_trigger>

  <.drawer_content id="actions-drawer-content" direction="bottom">
    <.drawer_header>
      <h2 id="actions-drawer-content-title" class="text-sm font-medium">
        Item Actions
      </h2>
    </.drawer_header>
    <.drawer_close />
    <div class="p-4 pb-8 space-y-2">
      <button phx-click="edit_item" class="w-full text-left px-4 py-3 rounded-md hover:bg-muted">
        Edit
      </button>
      <button phx-click="duplicate_item" class="w-full text-left px-4 py-3 rounded-md hover:bg-muted">
        Duplicate
      </button>
      <button phx-click="delete_item" class="w-full text-left px-4 py-3 rounded-md text-destructive hover:bg-destructive/10">
        Delete
      </button>
    </div>
  </.drawer_content>
</.drawer>

Example — Step-by-Step Form (left drawer)

Multi-step forms work well in a left drawer that feels like an overlay wizard:

<.drawer id="onboarding">
  <.drawer_trigger drawer_id="onboarding">
    <.button>Start Setup</.button>
  </.drawer_trigger>

  <.drawer_content id="onboarding-content" direction="left" open={@show_onboarding}>
    <.drawer_header>
      <h2 id="onboarding-content-title" class="text-lg font-semibold">
        Setup Wizard  Step {@step} of 3
      </h2>
    </.drawer_header>
    <.drawer_close />
    <div class="p-6">
      <%= render_step(@step) %>
    </div>
    <.drawer_footer>
      <button phx-click="prev_step" disabled={@step == 1}>Back</button>
      <button phx-click="next_step">
        {if @step == 3, do: "Finish", else: "Next"}
      </button>
    </.drawer_footer>
  </.drawer_content>
</.drawer>

Server-Controlled Open State

Pass open={@show_drawer} to drawer_content/1 to control visibility from the server (e.g. after a navigation event or background task completion):

<.drawer_content id="notif-content" direction="right" open={@show_notifications}>
  ...
</.drawer_content>

# Push from LiveView
{:noreply, assign(socket, :show_notifications, true)}

Hook Behaviour

The PhiaDrawer hook (mounted on drawer_content/1) handles:

  • CSS transform animation (slide in/out) on open/close
  • Focus trap — Tab / Shift+Tab cycle within the open panel
  • Escape key closes the panel and returns focus to the trigger
  • Backdrop click closes the panel
  • Focus return — restores focus to drawer_trigger/1 (or last focused element) when the drawer closes

Accessibility

  • drawer_content/1 has role="dialog" and aria-modal="true"
  • aria-labelledby="{id}-title" is auto-set — give your title heading the id "{drawer-content-id}-title" for the link to resolve
  • drawer_close/1 has aria-label="Close drawer" for screen reader users
  • Keyboard: focus automatically moves to the first focusable element on open

Summary

Functions

Root container for the Drawer. Wraps both drawer_trigger/1 and drawer_content/1. Provides the relative positioning context and a shared ID namespace.

The × close button rendered in the top-right corner of the drawer panel.

The drawer surface — renders the semi-transparent backdrop and the sliding panel. This is the PhiaDrawer hook mount point.

Action row at the bottom of the drawer.

Layout container for the drawer title and optional description.

Wrapper that opens the drawer when any element inside it is clicked.

Functions

drawer(assigns)

Root container for the Drawer. Wraps both drawer_trigger/1 and drawer_content/1. Provides the relative positioning context and a shared ID namespace.

The root is purely a layout wrapper — no hook attaches here. The hook mounts on drawer_content/1.

Attributes

  • id (:string) (required) - Unique drawer ID used as a namespace for child IDs.
  • class (:string) - Additional CSS classes. Defaults to nil.
  • Global attributes are accepted. Extra HTML attributes forwarded to the root <div>.

Slots

drawer_close(assigns)

The × close button rendered in the top-right corner of the drawer panel.

The PhiaDrawer JS hook listens for clicks on data-drawer-close and:

  1. Reverses the CSS transform animation (slides the panel out)
  2. Adds the hidden class to the root container after the animation
  3. Returns focus to the drawer_trigger/1 that opened the drawer

The opacity-70 default with hover:opacity-100 keeps the button present but unobtrusive, becoming fully visible on hover.

Attributes

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

drawer_content(assigns)

The drawer surface — renders the semi-transparent backdrop and the sliding panel. This is the PhiaDrawer hook mount point.

The outer element is fixed inset-0 and starts hidden when open={false}. It contains two children:

  • data-drawer-backdrop — the full-screen dark overlay (click to close)
  • data-drawer-panel — the sliding panel itself

The hook applies CSS transform: translate* to the panel to create the slide-in animation, then removes the hidden class from the container.

The aria-labelledby convention

The aria-labelledby is auto-set to "{id}-title". Give your title heading this exact ID so the reference resolves:

<.drawer_content id="settings-content" direction="right">
  <.drawer_header>
    <h2 id="settings-content-title">Settings</h2>
  </.drawer_header>
</.drawer_content>

Attributes

  • id (:string) (required) - Content element ID — the PhiaDrawer hook mount point. By convention: "{drawer_id}-content".

  • open (:boolean) - Whether the drawer is open on initial render. When false (default), the outer container has the hidden class. Pass open={@show_drawer} to control visibility from the server after the initial render.

    Defaults to false.

  • direction (:string) - Direction the drawer panel slides in from. Defaults to "bottom". Must be one of "bottom", "top", "left", or "right".

  • class (:string) - Additional CSS classes for the sliding panel. Defaults to nil.

  • Global attributes are accepted. Extra HTML attributes forwarded to the outer container <div>.

Slots

  • inner_block (required) - Drawer panel content: header, body, footer, close.

drawer_footer(assigns)

Action row at the bottom of the drawer.

Renders buttons in a flex row, right-aligned on desktop and stacked vertically (reversed order) on mobile for thumb accessibility. Use pt-0 to align naturally below the last content section.

<.drawer_footer>
  <button phx-click="cancel">Cancel</button>
  <button phx-click="save" class="...">Save</.button>
</.drawer_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 + action buttons.

drawer_header(assigns)

Layout container for the drawer title and optional description.

Provides p-6 padding and vertical space-y-1.5 between title and description. Always place at the top of the drawer content, before the scrollable body.

Attributes

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

Slots

  • inner_block (required) - Drawer heading and optional description paragraph.

drawer_trigger(assigns)

Wrapper that opens the drawer when any element inside it is clicked.

Renders a <div> (not a <button>) so you can place any trigger content inside — including a <.button> — without creating invalid nested-button HTML. The PhiaDrawer hook intercepts clicks on [data-drawer-trigger] anywhere in the document via event delegation, so the wrapper element type does not need to be focusable itself.

Example

<.drawer_trigger drawer_id="settings-drawer">
  <.button variant="outline">
    <.icon name="settings" class="mr-2" />
    Open Settings
  </.button>
</.drawer_trigger>

Attributes

  • drawer_id (:string) (required) - ID that matches the target drawer_content/1's id attribute. The hook looks for [data-drawer-trigger] whose value equals the drawer_content/1 element's id.

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

  • Global attributes are accepted. Extra HTML attributes forwarded to the wrapper <div>.

Slots

  • inner_block (required) - Trigger content — any element, typically a <.button>.