Drawer

Examples

Default

A mobile-first component that slides in from the edge of the screen.

<%= 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 %>

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>

Snap Points

Drawer with snap points for tiered content reveal (25%, 50%, 100%).

<%= 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 %>

Handle Only

Only the handle is draggable - content area is scrollable.

<%= 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 %>

Responsive

Shows Dialog on desktop (≥768px) and Drawer on mobile (<768px). Resize browser to see it switch.

<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>

Features

  • Custom styling with Tailwind classes
  • Focus management
  • Animation support

API Reference

Drawer

Parameters

NameTypeDefaultDescription
openBooleanfalseWhether the element is open
directionStringbottomThe direction
dismissibleBooleantrueThe dismissible
modalBooleantrueThe modal
snap_pointsStringnilThe snap points
active_snap_pointStringnilThe active snap point
fade_from_indexStringnilThe fade from index
snap_to_sequential_pointBooleanfalseThe snap to sequential point
handle_onlyBooleanfalseThe handle only
reposition_inputsBooleantrueThe reposition inputs

Close

Parameters

NameTypeDefaultDescription
variantSymbol:outlineVisual style variant
sizeSymbol:defaultSize of the element

Content

Parameters

NameTypeDefaultDescription
openBooleanfalseWhether the element is open
directionStringbottomThe direction

Description

Footer

Handle

Header

Overlay

Parameters

NameTypeDefaultDescription
openBooleanfalseWhether the element is open

Title

Trigger

Parameters

NameTypeDefaultDescription
as_childBooleanfalseWhen true, yields attributes to block instead of rendering wrapper
variantSymbol:outlineVisual style variant
sizeSymbol:defaultSize of the element

Accessibility

Implements the WAI-ARIA Dialog (Modal) pattern with proper roles, states, and keyboard navigation.

Keyboard Shortcuts

KeyDescription
EscapeCloses the component
EndMoves focus to last item

JavaScript

Stimulus Controller

ui--drawer

Values

NameTypeDescription
openBooleanControls open state

Actions

openclosecloseOnOverlayClickstartDragendDragsnapPointToPixelscalculateSnapPositionfindClosestSnapPointIndexsnapTosnapPointToPercentageisHorizontalDirectionisVerticalDirectionisClosingDirectioncalculateVelocityapplyDampingshouldIgnoreDragisHandleEventresetPositioncalculateOverlayOpacityshowhideanimateToClosedPositioncleanupAfterCloseisMobilehasSnapPointspositionAtClosedanimateToOpencleanupEscapeHandlerapplyTransformdispatchEvent