Drawer
Examples
Default
A mobile-first component that slides in from the edge of the screen.
Set your daily activity goal.
<%= render "ui/drawer" do %>
<%= render "ui/drawer/trigger" do %>Open Drawer<% end %>
<%= render "ui/drawer/overlay" %>
<%= render "ui/drawer/content" do %>
<%= render "ui/drawer/handle" %>
<%= render "ui/drawer/header" do %>
<%= render "ui/drawer/title" do %>Move Goal<% end %>
<%= render "ui/drawer/description" do %>Set your daily activity goal.<% end %>
<% end %>
<div class="p-4">Content goes here.</div>
<%= render "ui/drawer/footer" do %>
<%= render "ui/button" do %>Submit<% end %>
<%= render "ui/drawer/close" do %>Cancel<% end %>
<% end %>
<% end %>
<% end %>Set your daily activity goal.
<%= render UI::Drawer.new do %>
<%= render UI::DrawerTrigger.new { "Open Drawer" } %>
<%= render UI::DrawerOverlay.new %>
<%= render UI::DrawerContent.new do %>
<%= render UI::DrawerHandle.new %>
<%= render UI::DrawerHeader.new do %>
<%= render UI::DrawerTitle.new { "Move Goal" } %>
<%= render UI::DrawerDescription.new { "Set your daily activity goal." } %>
<% end %>
<div class="p-4">Content goes here.</div>
<%= render UI::DrawerFooter.new do %>
<%= render UI::Button.new { "Submit" } %>
<%= render UI::DrawerClose.new { "Cancel" } %>
<% end %>
<% end %>
<% end %>Set your daily activity goal.
<%= render UI::DrawerComponent.new do %>
<%= render(UI::DrawerTriggerComponent.new) { "Open Drawer" } %>
<%= render UI::DrawerOverlayComponent.new %>
<%= render UI::DrawerContentComponent.new do %>
<%= render UI::DrawerHandleComponent.new %>
<%= render UI::DrawerHeaderComponent.new do %>
<%= render(UI::DrawerTitleComponent.new) { "Move Goal" } %>
<%= render(UI::DrawerDescriptionComponent.new) { "Set your daily activity goal." } %>
<% end %>
<div class="p-4">Content goes here.</div>
<%= render UI::DrawerFooterComponent.new do %>
<%= render(UI::ButtonComponent.new) { "Submit" } %>
<%= render(UI::DrawerCloseComponent.new) { "Cancel" } %>
<% end %>
<% end %>
<% end %>Directions
Drawer can slide from any edge: top, bottom, left, or right.
<div class="flex gap-4">
<%= render "ui/drawer", direction: "left" do %>
<%= render "ui/drawer/trigger" do %>Left<% end %>
<%= render "ui/drawer/overlay" %>
<%= render "ui/drawer/content", direction: "left" do %>
<%= render "ui/drawer/header" do %>
<%= render "ui/drawer/title" do %>From Left<% end %>
<% end %>
<div class="p-4">Content</div>
<% end %>
<% end %>
<%= render "ui/drawer", direction: "right" do %>
<%= render "ui/drawer/trigger" do %>Right<% end %>
<%= render "ui/drawer/overlay" %>
<%= render "ui/drawer/content", direction: "right" do %>
<%= render "ui/drawer/header" do %>
<%= render "ui/drawer/title" do %>From Right<% end %>
<% end %>
<div class="p-4">Content</div>
<% end %>
<% end %>
</div><div class="flex gap-4">
<%= render UI::Drawer.new(direction: "left") do %>
<%= render UI::DrawerTrigger.new { "Left" } %>
<%= render UI::DrawerOverlay.new %>
<%= render UI::DrawerContent.new(direction: "left") do %>
<%= render UI::DrawerHeader.new do %>
<%= render UI::DrawerTitle.new { "From Left" } %>
<% end %>
<div class="p-4">Content</div>
<% end %>
<% end %>
<%= render UI::Drawer.new(direction: "right") do %>
<%= render UI::DrawerTrigger.new { "Right" } %>
<%= render UI::DrawerOverlay.new %>
<%= render UI::DrawerContent.new(direction: "right") do %>
<%= render UI::DrawerHeader.new do %>
<%= render UI::DrawerTitle.new { "From Right" } %>
<% end %>
<div class="p-4">Content</div>
<% end %>
<% end %>
</div><div class="flex gap-4">
<%= render UI::DrawerComponent.new(direction: "left") do %>
<%= render(UI::DrawerTriggerComponent.new) { "Left" } %>
<%= render UI::DrawerOverlayComponent.new %>
<%= render UI::DrawerContentComponent.new(direction: "left") do %>
<%= render UI::DrawerHeaderComponent.new do %>
<%= render(UI::DrawerTitleComponent.new) { "From Left" } %>
<% end %>
<div class="p-4">Content</div>
<% end %>
<% end %>
<%= render UI::DrawerComponent.new(direction: "right") do %>
<%= render(UI::DrawerTriggerComponent.new) { "Right" } %>
<%= render UI::DrawerOverlayComponent.new %>
<%= render UI::DrawerContentComponent.new(direction: "right") do %>
<%= render UI::DrawerHeaderComponent.new do %>
<%= render(UI::DrawerTitleComponent.new) { "From Right" } %>
<% end %>
<div class="p-4">Content</div>
<% end %>
<% end %>
</div>Snap Points
Drawer with snap points for tiered content reveal (25%, 50%, 100%).
Drag to different heights.
First: 25% viewport
Second: 50% viewport
Third: 100% viewport
<%= render "ui/drawer", modal: false, snap_points: [0.25, 0.5, 1], fade_from_index: 0 do %>
<%= render "ui/drawer/trigger" do %>Open with Snap Points<% end %>
<%= render "ui/drawer/overlay" %>
<%= render "ui/drawer/content", classes: "min-h-screen" do %>
<%= render "ui/drawer/handle" %>
<%= render "ui/drawer/header" do %>
<%= render "ui/drawer/title" do %>Snap Points<% end %>
<%= render "ui/drawer/description" do %>Drag to different heights.<% end %>
<% end %>
<div class="p-4 space-y-2">
<p class="text-sm">First: 25% viewport</p>
<p class="text-sm">Second: 50% viewport</p>
<p class="text-sm">Third: 100% viewport</p>
</div>
<% end %>
<% end %>Drag to different heights.
First: 25% viewport
Second: 50% viewport
Third: 100% viewport
<%= render UI::Drawer.new(modal: false, snap_points: [0.25, 0.5, 1], fade_from_index: 0) do %>
<%= render UI::DrawerTrigger.new { "Open with Snap Points" } %>
<%= render UI::DrawerOverlay.new %>
<%= render UI::DrawerContent.new(classes: "min-h-screen") do %>
<%= render UI::DrawerHandle.new %>
<%= render UI::DrawerHeader.new do %>
<%= render UI::DrawerTitle.new { "Snap Points" } %>
<%= render UI::DrawerDescription.new { "Drag to different heights." } %>
<% end %>
<div class="p-4 space-y-2">
<p class="text-sm">First: 25% viewport</p>
<p class="text-sm">Second: 50% viewport</p>
<p class="text-sm">Third: 100% viewport</p>
</div>
<% end %>
<% end %>Drag to different heights.
First: 25% viewport
Second: 50% viewport
Third: 100% viewport
<%= render UI::DrawerComponent.new(modal: false, snap_points: [0.25, 0.5, 1], fade_from_index: 0) do %>
<%= render(UI::DrawerTriggerComponent.new) { "Open with Snap Points" } %>
<%= render UI::DrawerOverlayComponent.new %>
<%= render UI::DrawerContentComponent.new(classes: "min-h-screen") do %>
<%= render UI::DrawerHandleComponent.new %>
<%= render UI::DrawerHeaderComponent.new do %>
<%= render(UI::DrawerTitleComponent.new) { "Snap Points" } %>
<%= render(UI::DrawerDescriptionComponent.new) { "Drag to different heights." } %>
<% end %>
<div class="p-4 space-y-2">
<p class="text-sm">First: 25% viewport</p>
<p class="text-sm">Second: 50% viewport</p>
<p class="text-sm">Third: 100% viewport</p>
</div>
<% end %>
<% end %>Handle Only
Only the handle is draggable - content area is scrollable.
Only the handle is draggable.
Drag the handle to close.
Scrollable item 1
Scrollable item 2
Scrollable item 3
Scrollable item 4
Scrollable item 5
<%= render "ui/drawer", handle_only: true do %>
<%= render "ui/drawer/trigger" do %>Open Handle-Only<% end %>
<%= render "ui/drawer/overlay" %>
<%= render "ui/drawer/content" do %>
<%= render "ui/drawer/handle" %>
<%= render "ui/drawer/header" do %>
<%= render "ui/drawer/title" do %>Handle-Only Mode<% end %>
<%= render "ui/drawer/description" do %>Only the handle is draggable.<% end %>
<% end %>
<div class="p-4 space-y-2" data-vaul-scrollable>
<p class="text-sm">Drag the handle to close.</p>
<% 5.times do |i| %>
<p class="text-sm text-muted-foreground">Scrollable item <%= i + 1 %></p>
<% end %>
</div>
<% end %>
<% end %>Only the handle is draggable.
Drag the handle to close.
Scrollable item 1
Scrollable item 2
Scrollable item 3
Scrollable item 4
Scrollable item 5
<%= render UI::Drawer.new(handle_only: true) do %>
<%= render UI::DrawerTrigger.new { "Open Handle-Only" } %>
<%= render UI::DrawerOverlay.new %>
<%= render UI::DrawerContent.new do %>
<%= render UI::DrawerHandle.new %>
<%= render UI::DrawerHeader.new do %>
<%= render UI::DrawerTitle.new { "Handle-Only Mode" } %>
<%= render UI::DrawerDescription.new { "Only the handle is draggable." } %>
<% end %>
<div class="p-4 space-y-2" data-vaul-scrollable>
<p class="text-sm">Drag the handle to close.</p>
<% 5.times do |i| %>
<p class="text-sm text-muted-foreground">Scrollable item <%= i + 1 %></p>
<% end %>
</div>
<% end %>
<% end %>Only the handle is draggable.
Drag the handle to close.
Scrollable item 1
Scrollable item 2
Scrollable item 3
Scrollable item 4
Scrollable item 5
<%= render UI::DrawerComponent.new(handle_only: true) do %>
<%= render(UI::DrawerTriggerComponent.new) { "Open Handle-Only" } %>
<%= render UI::DrawerOverlayComponent.new %>
<%= render UI::DrawerContentComponent.new do %>
<%= render UI::DrawerHandleComponent.new %>
<%= render UI::DrawerHeaderComponent.new do %>
<%= render(UI::DrawerTitleComponent.new) { "Handle-Only Mode" } %>
<%= render(UI::DrawerDescriptionComponent.new) { "Only the handle is draggable." } %>
<% end %>
<div class="p-4 space-y-2" data-vaul-scrollable>
<p class="text-sm">Drag the handle to close.</p>
<% 5.times do |i| %>
<p class="text-sm text-muted-foreground">Scrollable item <%= i + 1 %></p>
<% end %>
</div>
<% end %>
<% end %>Responsive
Shows Dialog on desktop (≥768px) and Drawer on mobile (<768px). Resize browser to see it switch.
Make changes to your profile.
Edit profile
Make changes to your profile.
<div data-controller="ui--responsive-dialog" data-ui--responsive-dialog-breakpoint-value="768">
<%# Mobile: Drawer %>
<div class="md:hidden" data-ui--responsive-dialog-target="drawer">
<%= render "ui/drawer" do %>
<%= render "ui/drawer/trigger" do %>Edit Profile<% end %>
<%= render "ui/drawer/overlay" %>
<%= render "ui/drawer/content" do %>
<%= render "ui/drawer/handle" %>
<%= render "ui/drawer/header", classes: "text-left" do %>
<%= render "ui/drawer/title" do %>Edit profile<% end %>
<%= render "ui/drawer/description" do %>Make changes to your profile.<% end %>
<% end %>
<div class="grid gap-4 px-4">
<div class="space-y-2">
<%= render "ui/label", for: "name-mobile" do %>Name<% end %>
<%= render "ui/input", id: "name-mobile", value: "Pedro Duarte" %>
</div>
</div>
<%= render "ui/drawer/footer", classes: "pt-2" do %>
<%= render "ui/button" do %>Save<% end %>
<%= render "ui/drawer/close" do %>Cancel<% end %>
<% end %>
<% end %>
<% end %>
</div>
<%# Desktop: Dialog %>
<div class="hidden md:block" data-ui--responsive-dialog-target="dialog">
<%= render "ui/dialog" do %>
<%= render "ui/dialog/trigger" do %>Edit Profile<% end %>
<%= render "ui/dialog/overlay" %>
<%= render "ui/dialog/content" do %>
<%= render "ui/dialog/header" do %>
<%= render "ui/dialog/title" do %>Edit profile<% end %>
<%= render "ui/dialog/description" do %>Make changes to your profile.<% end %>
<% end %>
<div class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<%= render "ui/label", for: "name-desktop", classes: "text-right" do %>Name<% end %>
<%= render "ui/input", id: "name-desktop", value: "Pedro Duarte", classes: "col-span-3" %>
</div>
</div>
<%= render "ui/dialog/footer" do %>
<%= render "ui/button" do %>Save<% end %>
<% end %>
<% end %>
<% end %>
</div>
</div>Make changes to your profile.
Edit profile
Make changes to your profile.
<div data-controller="ui--responsive-dialog" data-ui--responsive-dialog-breakpoint-value="768">
<%# Mobile: Drawer %>
<div class="md:hidden" data-ui--responsive-dialog-target="drawer">
<%= render UI::Drawer.new do %>
<%= render UI::DrawerTrigger.new { "Edit Profile" } %>
<%= render UI::DrawerOverlay.new %>
<%= render UI::DrawerContent.new do %>
<%= render UI::DrawerHandle.new %>
<%= render UI::DrawerHeader.new(classes: "text-left") do %>
<%= render UI::DrawerTitle.new { "Edit profile" } %>
<%= render UI::DrawerDescription.new { "Make changes to your profile." } %>
<% end %>
<div class="grid gap-4 px-4">
<div class="space-y-2">
<%= render UI::Label.new(for: "name-mobile-phlex") { "Name" } %>
<%= render UI::Input.new(id: "name-mobile-phlex", value: "Pedro Duarte") %>
</div>
</div>
<%= render UI::DrawerFooter.new(classes: "pt-2") do %>
<%= render UI::Button.new { "Save" } %>
<%= render UI::DrawerClose.new { "Cancel" } %>
<% end %>
<% end %>
<% end %>
</div>
<%# Desktop: Dialog %>
<div class="hidden md:block" data-ui--responsive-dialog-target="dialog">
<%= render UI::Dialog.new do %>
<%= render UI::DialogTrigger.new { "Edit Profile" } %>
<%= render UI::DialogOverlay.new %>
<%= render UI::DialogContent.new do %>
<%= render UI::DialogHeader.new do %>
<%= render UI::DialogTitle.new { "Edit profile" } %>
<%= render UI::DialogDescription.new { "Make changes to your profile." } %>
<% end %>
<div class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<%= render UI::Label.new(for: "name-desktop-phlex", classes: "text-right") { "Name" } %>
<%= render UI::Input.new(id: "name-desktop-phlex", value: "Pedro Duarte", classes: "col-span-3") %>
</div>
</div>
<%= render UI::DialogFooter.new do %>
<%= render UI::Button.new { "Save" } %>
<% end %>
<% end %>
<% end %>
</div>
</div>Make changes to your profile.
Edit profile
Make changes to your profile.
<div data-controller="ui--responsive-dialog" data-ui--responsive-dialog-breakpoint-value="768">
<%# Mobile: Drawer %>
<div class="md:hidden" data-ui--responsive-dialog-target="drawer">
<%= render UI::DrawerComponent.new do %>
<%= render(UI::DrawerTriggerComponent.new) { "Edit Profile" } %>
<%= render UI::DrawerOverlayComponent.new %>
<%= render UI::DrawerContentComponent.new do %>
<%= render UI::DrawerHandleComponent.new %>
<%= render UI::DrawerHeaderComponent.new(classes: "text-left") do %>
<%= render(UI::DrawerTitleComponent.new) { "Edit profile" } %>
<%= render(UI::DrawerDescriptionComponent.new) { "Make changes to your profile." } %>
<% end %>
<div class="grid gap-4 px-4">
<div class="space-y-2">
<%= render(UI::LabelComponent.new(for: "name-mobile-vc")) { "Name" } %>
<%= render UI::InputComponent.new(id: "name-mobile-vc", value: "Pedro Duarte") %>
</div>
</div>
<%= render UI::DrawerFooterComponent.new(classes: "pt-2") do %>
<%= render(UI::ButtonComponent.new) { "Save" } %>
<%= render(UI::DrawerCloseComponent.new) { "Cancel" } %>
<% end %>
<% end %>
<% end %>
</div>
<%# Desktop: Dialog %>
<div class="hidden md:block" data-ui--responsive-dialog-target="dialog">
<%= render UI::DialogComponent.new do %>
<%= render(UI::DialogTriggerComponent.new) { "Edit Profile" } %>
<%= render UI::DialogOverlayComponent.new %>
<%= render UI::DialogContentComponent.new do %>
<%= render UI::DialogHeaderComponent.new do %>
<%= render(UI::DialogTitleComponent.new) { "Edit profile" } %>
<%= render(UI::DialogDescriptionComponent.new) { "Make changes to your profile." } %>
<% end %>
<div class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<%= render(UI::LabelComponent.new(for: "name-desktop-vc", classes: "text-right")) { "Name" } %>
<%= render UI::InputComponent.new(id: "name-desktop-vc", value: "Pedro Duarte", classes: "col-span-3") %>
</div>
</div>
<%= render UI::DialogFooterComponent.new do %>
<%= render(UI::ButtonComponent.new) { "Save" } %>
<% end %>
<% end %>
<% end %>
</div>
</div>Features
- Custom styling with Tailwind classes
- Focus management
- Animation support
API Reference
Drawer
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
| open | Boolean | false | Whether the element is open |
| direction | String | bottom | The direction |
| dismissible | Boolean | true | The dismissible |
| modal | Boolean | true | The modal |
| snap_points | String | nil | The snap points |
| active_snap_point | String | nil | The active snap point |
| fade_from_index | String | nil | The fade from index |
| snap_to_sequential_point | Boolean | false | The snap to sequential point |
| handle_only | Boolean | false | The handle only |
| reposition_inputs | Boolean | true | The reposition inputs |
Close
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
| variant | Symbol | :outline | Visual style variant |
| size | Symbol | :default | Size of the element |
Content
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
| open | Boolean | false | Whether the element is open |
| direction | String | bottom | The direction |
Description
Footer
Handle
Header
Overlay
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
| open | Boolean | false | Whether the element is open |
Title
Trigger
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
| as_child | Boolean | false | When true, yields attributes to block instead of rendering wrapper |
| variant | Symbol | :outline | Visual style variant |
| size | Symbol | :default | Size of the element |
Accessibility
Implements the WAI-ARIA Dialog (Modal) pattern with proper roles, states, and keyboard navigation.
Keyboard Shortcuts
| Key | Description |
|---|---|
| Escape | Closes the component |
| End | Moves focus to last item |
JavaScript
Stimulus Controller
ui--drawerValues
| Name | Type | Description |
|---|---|---|
| open | Boolean | Controls open state |
Actions
openclosecloseOnOverlayClickstartDragendDragsnapPointToPixelscalculateSnapPositionfindClosestSnapPointIndexsnapTosnapPointToPercentageisHorizontalDirectionisVerticalDirectionisClosingDirectioncalculateVelocityapplyDampingshouldIgnoreDragisHandleEventresetPositioncalculateOverlayOpacityshowhideanimateToClosedPositioncleanupAfterCloseisMobilehasSnapPointspositionAtClosedanimateToOpencleanupEscapeHandlerapplyTransformdispatchEvent