Drawer
NewOverview
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.
<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.
<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.
<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.
<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.
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
<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.
<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.
<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>
Accessibility
The native modal dialog traps focus, makes the surrounding page inert, and restores focus
to the trigger. Escape and backdrop dismissal are enabled by default and can be disabled independently. The component uses the native
dialog
element.
Add this module
mix dizzy.add drawer
Component API
Drawer.drawer
Attributes
| Name | Type | Default | 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
| Name | Required |
|---|---|
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.