Dialog
A modal overlay with a backdrop, animated panel, and close button — composes trigger, content, header, footer, title, and description helpers.
Installation
bin/rails g shadcnrb:component dialog
Usage
Wrap everything in sui.dialog do |dialog|. The proxy yields child helpers (dialog.trigger, dialog.content, ...) — they're not on sui.
Welcome
This is a basic dialog example.
Dialog body content goes here.
<%= sui.dialog do |dialog| %> <%= dialog.trigger "Open dialog", variant: :outline %> <%= dialog.content do %> <%= dialog.header do %> <%= dialog.title "Welcome" %> <%= dialog.description "This is a basic dialog example." %> <% end %> <p class="text-sm">Dialog body content goes here.</p> <% end %> <% end %>
Confirmation dialog
Use dialog.footer to place action buttons at the bottom.
Are you sure?
This action cannot be undone.
<%= sui.dialog do |dialog| %> <%= dialog.trigger "Delete account", variant: :destructive %> <%= dialog.content do %> <%= dialog.header do %> <%= dialog.title "Are you sure?" %> <%= dialog.description "This action cannot be undone." %> <% end %> <%= dialog.footer do %> <%= dialog.close "Cancel", variant: :outline %> <%= dialog.close "Yes, delete", variant: :destructive %> <% end %> <% end %> <% end %>
Profile edit dialog
Compose form fields inside the content area using sui.label and sui.input.
Edit profile
Update your display name and email address.
<%= sui.dialog do |dialog| %> <%= dialog.trigger "Edit profile", variant: :outline %> <%= dialog.content do %> <%= dialog.header do %> <%= dialog.title "Edit profile" %> <%= dialog.description "Update your display name and email address." %> <% end %> <div class="grid gap-4 py-2"> <div class="grid gap-1.5"> <%= sui.label "Name", for: "dialog-name" %> <%= sui.input id: "dialog-name", placeholder: "Your name" %> </div> <div class="grid gap-1.5"> <%= sui.label "Email", for: "dialog-email" %> <%= sui.input id: "dialog-email", type: "email", placeholder: "[email protected]" %> </div> </div> <%= dialog.footer do %> <%= sui.button "Save changes" %> <% end %> <% end %> <% end %>
Lazy-loaded content
Pass src: to
dialog.content and the dialog
renders a Turbo Frame that fetches content when opened. The block becomes the loading state.
Loading profile...
<%= sui.dialog do |dialog| %> <%= dialog.trigger "Lazy-load demo", variant: :outline %> <%= dialog.content(src: lazy_dialog_content_path) do %> <div class="flex items-center justify-center p-8"> <p class="text-sm text-muted-foreground animate-pulse">Loading profile...</p> </div> <% end %> <% end %>
The server endpoint wraps its response in a matching
<turbo-frame> tag.
Content loads once on first open, then stays cached for subsequent opens. The lazy-loaded
partial reaches dialog parts via sui.dialog_proxy.
Reload on every open
Pass reload: true to re-fetch
content every time the dialog opens, instead of caching the first response. The loading
state is restored on close so the user sees it again on the next open.
Loading profile...
<%= sui.dialog do |dialog| %> <%= dialog.trigger "Reload demo", variant: :outline %> <%= dialog.content(src: lazy_dialog_content_path, reload: true) do %> <div class="flex items-center justify-center p-8"> <p class="text-sm text-muted-foreground animate-pulse">Loading profile...</p> </div> <% end %> <% end %>
Open this dialog multiple times. Each open shows "Loading..." briefly, then fetches fresh content from the server (with a 1s simulated delay).
Server-driven confirmation
Sometimes the server needs to warn the user mid-request. The delete action hits the
server, the server detects the operation is risky (e.g., chart has active users), and
responds with a Turbo Stream that appends a confirmation dialog. The OK button re-submits
the same request with confirm=true, and this time the server does the work.
Try both: chart 1 is "risky" (shows confirmation), chart 2 is "safe" (deletes immediately).
<div class="flex gap-2"> <%= button_to "Delete chart 1 (risky)", destroy_demo_chart_path(1), method: :delete, form: { data: { turbo_stream: true } }, class: "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap cursor-pointer h-9 px-4 py-2 bg-destructive text-white hover:bg-destructive/90" %> <%= button_to "Delete chart 2 (safe)", destroy_demo_chart_path(2), method: :delete, form: { data: { turbo_stream: true } }, class: "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap cursor-pointer h-9 px-4 py-2 border bg-background shadow-xs hover:bg-accent" %> </div>
API
| Method | Args | Default | Description |
|---|---|---|---|
sui.dialog | open: | false | Root container; yields a dialog proxy. Set open: true to start open |
sui.dialog_proxy | — | — | Returns a bare proxy for lazy-loaded partials rendered inside a Turbo Frame |
dialog.trigger | name, variant:, size: | :default, :md | Button that opens the dialog panel on click |
dialog.content | src:, reload:, **opts | nil, false | Backdrop + centered panel. src: lazy-loads via Turbo Frame. reload: true re-fetches on every open |
dialog.close | name, **opts | — | Wraps content so clicking it closes the dialog (use in footer) |
dialog.header | **opts | — | Flex column container for title and description |
dialog.footer | **opts | — | Row container for action buttons, right-aligned on sm+ |
dialog.title | name | nil | Semibold heading rendered as h2 |
dialog.description | name | nil | Muted supporting text rendered as p |