Blocks

Drawer

A slide-in panel anchored to any edge of the viewport — supports right, left, top, and bottom sides with animated open/close transitions.

Installation

bin/rails g shadcnrb:component drawer

Usage

The block is the drawer panel; drawer.trigger marks what opens it — the same shape as sui.dialog. Child parts (drawer.header, …) live on the proxy.

Drawer

Side panel content goes here.

Body content.
<%= sui.drawer do |drawer| %>
  <%= drawer.trigger { sui.button "Open drawer", variant: :outline } %>
  <%= drawer.header do %>
    <%= drawer.title "Drawer" %>
    <%= drawer.description "Side panel content goes here." %>
  <% end %>
  <div class="p-4 text-sm">Body content.</div>
<% end %>

Sides

Pass side: to sui.drawer — one of :right (default), :left, :top, :bottom.

Right drawer

Slides in from the right.

Content panel.

Left drawer

Slides in from the left.

Content panel.

Top drawer

Slides down from the top.

Content panel.

Bottom drawer

Slides up from the bottom.

Content panel.
<div class="flex flex-wrap gap-2">
  <%= sui.drawer do |drawer| %>
    <%= drawer.trigger { sui.button "Right (default)", variant: :outline } %>
    <%= drawer.header do %>
      <%= drawer.title "Right drawer" %>
      <%= drawer.description "Slides in from the right." %>
    <% end %>
    <div class="p-4 text-sm">Content panel.</div>
  <% end %>

  <%= sui.drawer side: :left do |drawer| %>
    <%= drawer.trigger { sui.button "Left", variant: :outline } %>
    <%= drawer.header do %>
      <%= drawer.title "Left drawer" %>
      <%= drawer.description "Slides in from the left." %>
    <% end %>
    <div class="p-4 text-sm">Content panel.</div>
  <% end %>

  <%= sui.drawer side: :top do |drawer| %>
    <%= drawer.trigger { sui.button "Top", variant: :outline } %>
    <%= drawer.header do %>
      <%= drawer.title "Top drawer" %>
      <%= drawer.description "Slides down from the top." %>
    <% end %>
    <div class="p-4 text-sm">Content panel.</div>
  <% end %>

  <%= sui.drawer side: :bottom do |drawer| %>
    <%= drawer.trigger { sui.button "Bottom", variant: :outline } %>
    <%= drawer.header do %>
      <%= drawer.title "Bottom drawer" %>
      <%= drawer.description "Slides up from the bottom." %>
    <% end %>
    <div class="p-4 text-sm">Content panel.</div>
  <% end %>
</div>

Settings drawer

A practical example with a header, scrollable body, and sticky footer.

Settings

Manage your account preferences.

<%= sui.drawer do |drawer| %>
  <%= drawer.trigger { sui.button "Settings", variant: :outline } %>
  <%= drawer.header do %>
    <%= drawer.title "Settings" %>
    <%= drawer.description "Manage your account preferences." %>
  <% end %>
  <div class="flex-1 overflow-y-auto p-4 space-y-4">
    <div class="flex items-center justify-between">
      <%= sui.label "Dark mode" %>
      <%= sui.switch name: "dark_mode" %>
    </div>
    <div class="flex items-center justify-between">
      <%= sui.label "Notifications" %>
      <%= sui.switch name: "notifications", checked: true %>
    </div>
  </div>
  <%= drawer.footer do %>
    <%= sui.button "Save changes" %>
  <% end %>
<% end %>

Lazy loading and triggers

Drawer takes the same lazy-loading and trigger options as sui.dialog: src: / reload: / loading: fetch the panel via a Turbo Frame, the drawer: kwarg on sui.button takes a Hash to wrap the button or a String id to open a drawer from anywhere in the page.

Loading...

<%= sui.button "Lazy drawer", variant: :outline,
      drawer: { src: lazy_hover_card_content_path,
                loading: sui.skeleton(class: "h-24") } %>

<%= sui.button "Detached trigger", variant: :outline, drawer: "detached-demo-drawer" %>
<%= sui.drawer src: lazy_hover_card_content_path, side: :left, id: "detached-demo-drawer" %>

API

MethodArgsDefaultDescription
sui.drawerside:, src:, reload:, loading:, content:, **opts:rightWrapper; the block body is the panel (backdrop + edge-anchored sheet). side: is :right :left :top :bottom; content: is the panel's option hash. src: lazy-loads the panel via Turbo Frame (body or loading: = loading state)
drawer.trigger&blockMarks which part of the block is the trigger; your markup, verbatim — click-to-open is bound on the wrapper. Mark one element data-slot="drawer-trigger" to scope it in composite triggers
drawer.header**optsPadded flex column for title and description
drawer.footer**optsSticky bottom area with padding for action buttons
drawer.titlenamenilSemibold heading rendered as h2
drawer.descriptionnamenilMuted supporting text rendered as p