Drawer

New

Overview

A modal panel that slides from a viewport edge, with native dialog focus management and four placements.

The drawer uses the native HTML dialog element for modal behavior. Use a right or left drawer for navigation, filters, and detail views. Use a top or bottom drawer for compact actions and mobile layouts.

Project details

Review the current status and owner.
Project
Component library
Owner
Alex Morgan
Status
In progress
HEEx
<div class="flex justify-center">
  <Button.button kind={:outline} phx-click={Drawer.show("example-drawer")}>
    Open drawer
  </Button.button>
  <Drawer.drawer id="example-drawer">
    <:title>Project details</:title>
    <:description>Review the current status and owner.</:description>
    <dl class="grid gap-5 text-sm">
      <div>
        <dt class="text-muted-foreground">Project</dt>
        <dd class="mt-1 font-medium">Component library</dd>
      </div>
      <div>
        <dt class="text-muted-foreground">Owner</dt>
        <dd class="mt-1 font-medium">Alex Morgan</dd>
      </div>
      <div>
        <dt class="text-muted-foreground">Status</dt>
        <dd class="mt-1 font-medium">In progress</dd>
      </div>
    </dl>
    <:footer>
      <Button.button kind={:ghost} phx-click={Drawer.hide("example-drawer")}>Close</Button.button>
      <Button.button phx-click={Drawer.hide("example-drawer")}>Open project</Button.button>
    </:footer>
  </Drawer.drawer>
</div>

Placements

Set side to :top, :right, :bottom, or :left. Side drawers fill the viewport height; top and bottom drawers size to their content up to the viewport limit.

Top drawer

This panel is anchored to the top edge.

Drawer content can contain navigation, filters, forms, or supporting details.

Right drawer

This panel is anchored to the right edge.

Drawer content can contain navigation, filters, forms, or supporting details.

Bottom drawer

This panel is anchored to the bottom edge.

Drawer content can contain navigation, filters, forms, or supporting details.

Left drawer

This panel is anchored to the left edge.

Drawer content can contain navigation, filters, forms, or supporting details.

HEEx
<div class="flex flex-wrap justify-center gap-2">
  <div :for={{label, side} <- [{"Top", :top}, {"Right", :right}, {"Bottom", :bottom}, {"Left", :left}]}>
    <Button.button
      kind={:outline}
      phx-click={Drawer.show("placement-drawer-#{side}")}
    >
      {label}
    </Button.button>
    <Drawer.drawer id={"placement-drawer-#{side}"} side={side}>
      <:title>{label} drawer</:title>
      <:description>This panel is anchored to the {String.downcase(label)} edge.</:description>
      <p class="text-sm leading-6 text-muted-foreground">
        Drawer content can contain navigation, filters, forms, or supporting details.
      </p>
    </Drawer.drawer>
  </div>
</div>

Backdrop dismissal

Set close_on_backdrop=false when an outside click should not discard the current work. Other close controls, including Escape, remain available.

Unsaved changes

The backdrop cannot dismiss this drawer.

Clicking outside leaves the drawer open. The close button and Escape key still work.

HEEx
<div class="flex justify-center">
  <Button.button kind={:outline} phx-click={Drawer.show("backdrop-dismissal-drawer")}>
    Open protected draft
  </Button.button>
  <Drawer.drawer id="backdrop-dismissal-drawer" close_on_backdrop={false}>
    <:title>Unsaved changes</:title>
    <:description>The backdrop cannot dismiss this drawer.</:description>
    <p class="text-sm leading-6 text-muted-foreground">
      Clicking outside leaves the drawer open. The close button and Escape key still work.
    </p>
    <:footer>
      <Button.button phx-click={Drawer.hide("backdrop-dismissal-drawer")}>
        Save and close
      </Button.button>
    </:footer>
  </Drawer.drawer>
</div>

Escape dismissal

Set close_on_escape=false when Escape must not interrupt the current workflow. The backdrop and explicit close controls still dismiss the drawer.

Required review

Escape cannot dismiss this drawer.

Pressing Escape leaves the drawer open. The close button and backdrop still work.

HEEx
<div class="flex justify-center">
  <Button.button kind={:outline} phx-click={Drawer.show("escape-dismissal-drawer")}>
    Open required review
  </Button.button>
  <Drawer.drawer id="escape-dismissal-drawer" close_on_escape={false}>
    <:title>Required review</:title>
    <:description>Escape cannot dismiss this drawer.</:description>
    <p class="text-sm leading-6 text-muted-foreground">
      Pressing Escape leaves the drawer open. The close button and backdrop still work.
    </p>
    <:footer>
      <Button.button phx-click={Drawer.hide("escape-dismissal-drawer")}>
        Finish review
      </Button.button>
    </:footer>
  </Drawer.drawer>
</div>

LiveView control

Bind open to a LiveView assign and update it from server events. This example disables the three local dismissal controls so the server is the only source of open and closed state.

LiveView
def mount(_params, _session, socket) do
  {:ok, assign(socket, :drawer_open, false)}
end

def handle_event("open-liveview-drawer", _params, socket) do
  {:noreply, assign(socket, :drawer_open, true)}
end

def handle_event("close-liveview-drawer", _params, socket) do
  {:noreply, assign(socket, :drawer_open, false)}
end

Export workspace

LiveView controls this drawer while the export is prepared.

Server events can change the same assign to open or close the drawer.

HEEx
<div class="flex justify-center">
  <Button.button phx-click="open-liveview-drawer">Open from LiveView</Button.button>
  <Drawer.drawer
    id="liveview-controlled-drawer"
    open={@drawer_open}
    close_on_backdrop={false}
    close_on_escape={false}
    show_close={false}
  >
    <:title>Export workspace</:title>
    <:description>LiveView controls this drawer while the export is prepared.</:description>
    <p class="text-sm leading-6 text-muted-foreground">
      Server events can change the same assign to open or close the drawer.
    </p>
    <:footer>
      <Button.button phx-click="close-liveview-drawer">Finish and close</Button.button>
    </:footer>
  </Drawer.drawer>
</div>

Sizing

Use class to replace the default perpendicular size. Tailwind utilities such as w-80, w-[28rem], or w-full max-w-xl set the width of left and right drawers. Include a mobile width or maximum when the fixed width could exceed a narrow viewport.

Compact drawer

The drawer width is set with Tailwind classes.

Use the class attribute to fit the drawer to its content and workflow.

Comfortable drawer

The drawer width is set with Tailwind classes.

Use the class attribute to fit the drawer to its content and workflow.

Wide drawer

The drawer width is set with Tailwind classes.

Use the class attribute to fit the drawer to its content and workflow.

HEEx
<div class="flex flex-wrap justify-center gap-2">
  <div :for={{label, id, width} <- [
    {"Compact", "compact", "w-80"},
    {"Comfortable", "comfortable", "w-[28rem]"},
    {"Wide", "wide", "w-full max-w-xl"}
  ]}>
    <Button.button kind={:outline} phx-click={Drawer.show("#{id}-drawer")}>
      {label}
    </Button.button>
    <Drawer.drawer id={"#{id}-drawer"} class={width}>
      <:title>{label} drawer</:title>
      <:description>The drawer width is set with Tailwind classes.</:description>
      <p class="text-sm leading-6 text-muted-foreground">
        Use the class attribute to fit the drawer to its content and workflow.
      </p>
    </Drawer.drawer>
  </div>
</div>

Sticky header and footer

Long content scrolls inside the content region. The title, description, close control, and footer remain visible while the middle region scrolls.

Workspace settings

Review notification and access settings.
  • Weekly summary

    Choose how your workspace handles this notification.

  • Product announcements

    Choose how your workspace handles this notification.

  • Comment notifications

    Choose how your workspace handles this notification.

  • Mention notifications

    Choose how your workspace handles this notification.

  • Task assignments

    Choose how your workspace handles this notification.

  • Review requests

    Choose how your workspace handles this notification.

  • Workspace invitations

    Choose how your workspace handles this notification.

  • Security alerts

    Choose how your workspace handles this notification.

  • Billing receipts

    Choose how your workspace handles this notification.

  • Export completion

    Choose how your workspace handles this notification.

  • Integration failures

    Choose how your workspace handles this notification.

  • API usage alerts

    Choose how your workspace handles this notification.

  • Member changes

    Choose how your workspace handles this notification.

  • Project archiving

    Choose how your workspace handles this notification.

  • Retention reminders

    Choose how your workspace handles this notification.

  • Maintenance notices

    Choose how your workspace handles this notification.

HEEx
<div class="flex justify-center">
  <Button.button kind={:outline} phx-click={Drawer.show("scrolling-drawer")}>
    Open settings
  </Button.button>
  <Drawer.drawer id="scrolling-drawer" class="w-[calc(100vw-1rem)] sm:w-[32rem]">
    <:title>Workspace settings</:title>
    <:description>Review notification and access settings.</:description>
    <ul class="-my-3 divide-y divide-border">
      <li :for={setting <- [
        "Weekly summary",
        "Product announcements",
        "Comment notifications",
        "Mention notifications",
        "Task assignments",
        "Review requests",
        "Workspace invitations",
        "Security alerts",
        "Billing receipts",
        "Export completion",
        "Integration failures",
        "API usage alerts",
        "Member changes",
        "Project archiving",
        "Retention reminders",
        "Maintenance notices"
      ]} class="py-4">
        <p class="text-sm font-medium">{setting}</p>
        <p class="mt-1 text-sm leading-6 text-muted-foreground">
          Choose how your workspace handles this notification.
        </p>
      </li>
    </ul>
    <:footer>
      <Button.button kind={:ghost} phx-click={Drawer.hide("scrolling-drawer")}>
        Cancel
      </Button.button>
      <Button.button phx-click={Drawer.hide("scrolling-drawer")}>
        Save settings
      </Button.button>
    </:footer>
  </Drawer.drawer>
</div>

Add this module

terminal
mix dizzy.add drawer

Component API

Drawer.drawer

Attributes

Attributes for Drawer.drawer
NameTypeDefault Values
class :any nilโ€”
close_label :string "Close drawer"โ€”
close_on_backdrop :boolean trueโ€”
close_on_escape :boolean trueโ€”
id :string Requiredโ€”
open :boolean nilโ€”
rest :global nilโ€”
show_close :boolean trueโ€”
side :atom :right:top, :right, :bottom, :left

Slots

Slots for Drawer.drawer
NameRequired
description Optional
footer Optional
inner_block Required
title Required

JavaScript hook

DizzyDrawer

Opens and closes the native dialog element from Phoenix JS commands or a server-rendered open value and coordinates the slide transition. Escape and backdrop dismissal are independently configurable. The browser manages focus, top-layer stacking, and background inertness.

mix dizzy.add includes the hook and performs any required asset setup automatically.

Explore Dizzy