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.
<%= 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.
Left drawer
Slides in from the left.
Top drawer
Slides down from the top.
Bottom drawer
Slides up from the bottom.
<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 %>
Inset sheet
A floating sheet with a gap around it is just classes on the panel. The
sides already anchor it with inset-y-0, so a margin
insets it; h-auto lets the top and bottom insets set
the height instead of h-full, and
rounded-xl border closes the frame on the side that
no longer touches the edge.
Inset sheet
Floats with a gap on every side.
<%= sui.drawer content: { class: "m-4 h-auto rounded-xl border translate-x-[calc(100%+1rem)]" } do |drawer| %> <%= drawer.trigger { sui.button "Inset sheet", variant: :outline } %> <%= drawer.header do %> <%= drawer.title "Inset sheet" %> <%= drawer.description "Floats with a gap on every side." %> <% end %> <% end %>
Sizing
Left and right sheets are w-3/4 max-w-sm; top and
bottom already take their height from the content. Classes on
content: replace those defaults, so
w-fit gives a sheet as wide as what is in it.
Jump to
<%= sui.drawer content: { class: "w-fit" } do |drawer| %> <%= drawer.trigger { sui.button "Content-sized", variant: :outline } %> <%= drawer.header do %> <%= drawer.title "Jump to" %> <% end %> <nav class="flex flex-col gap-1 p-4 pt-0"> <% %w[Dashboard Reports Settings].each do |item| %> <%= sui.link_to item, "#", variant: :ghost, class: "justify-start" %> <% end %> </nav> <% end %>
For a width that follows the page, set the fractions per breakpoint and
lift the default cap with max-w-none.
Responsive sheet
Full width on a phone, a third of the page on a wide screen.
<%= sui.drawer content: { class: "w-full max-w-none sm:w-2/3 lg:w-1/2 xl:w-1/3" } do |drawer| %> <%= drawer.trigger { sui.button "Responsive width", variant: :outline } %> <%= drawer.header do %> <%= drawer.title "Responsive sheet" %> <%= drawer.description "Full width on a phone, a third of the page on a wide screen." %> <% end %> <% end %>
A bottom sheet already spans the viewport, so a max width plus
sm:mx-auto turns it into a centred panel on larger
screens — one component, phone and desktop layouts.
Share
Anyone with the link can view this page.
<%= sui.drawer side: :bottom, content: { class: "sm:max-w-md sm:mx-auto sm:rounded-t-xl sm:border-x" } do |drawer| %> <%= drawer.trigger { sui.button "Bottom sheet", variant: :outline } %> <%= drawer.header do %> <%= drawer.title "Share" %> <%= drawer.description "Anyone with the link can view this page." %> <% end %> <%= drawer.footer do %> <%= drawer.close "Done" %> <% 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
| Method | Args | Default | Description |
|---|---|---|---|
sui.drawer | side:, src:, reload:, loading:, content:, **opts | :right | Wrapper; 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) |
sui.drawer_proxy | — | — | Returns a bare proxy for lazy-loaded partials rendered inside a Turbo Frame |
drawer.trigger | &block | — | Marks 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.close | name, **opts | — | Wraps content so clicking it closes the drawer (use in footer) |
drawer.header | **opts | — | Padded flex column for title and description |
drawer.footer | **opts | — | Sticky bottom area with padding for action buttons |
drawer.title | name | nil | Semibold heading rendered as h2 |
drawer.description | name | nil | Muted supporting text rendered as p |