Blocks

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

MethodArgsDefaultDescription
sui.dialogopen:falseRoot container; yields a dialog proxy. Set open: true to start open
sui.dialog_proxyReturns a bare proxy for lazy-loaded partials rendered inside a Turbo Frame
dialog.triggername, variant:, size::default, :mdButton that opens the dialog panel on click
dialog.contentsrc:, reload:, **optsnil, falseBackdrop + centered panel. src: lazy-loads via Turbo Frame. reload: true re-fetches on every open
dialog.closename, **optsWraps content so clicking it closes the dialog (use in footer)
dialog.header**optsFlex column container for title and description
dialog.footer**optsRow container for action buttons, right-aligned on sm+
dialog.titlenamenilSemibold heading rendered as h2
dialog.descriptionnamenilMuted supporting text rendered as p