Blocks

Dialog

A modal overlay with a backdrop, animated panel, and close button — composes trigger, content, header, footer, title, and description helpers.

Usage

Wrap everything in sui.dialog. The trigger opens the panel; the content holds the body.

Welcome

This is a basic dialog example.

Dialog body content goes here.

sui.dialog do
  sui.dialog_trigger "Open dialog", variant: :outline
  sui.dialog_content do
    sui.dialog_header do
      sui.dialog_title "Welcome"
      sui.dialog_description "This is a basic dialog example."
    end
    tag.p "Dialog body content goes here.", class: "text-sm"
  end
end

Confirmation dialog

Use sui.dialog_footer to place action buttons at the bottom.

Are you sure?

This action cannot be undone.

sui.dialog do
  sui.dialog_trigger "Delete account", variant: :destructive
  sui.dialog_content do
    sui.dialog_header do
      sui.dialog_title "Are you sure?"
      sui.dialog_description "This action cannot be undone."
    end
    sui.dialog_footer do
      sui.dialog_close "Cancel", variant: :outline
      sui.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
  sui.dialog_trigger "Edit profile", variant: :outline
  sui.dialog_content do
    sui.dialog_header do
      sui.dialog_title "Edit profile"
      sui.dialog_description "Update your display name and email address."
    end
    tag.div(class: "grid gap-4 py-2") do
      tag.div(class: "grid gap-1.5") do
        sui.label("Name", for: "name") +
        sui.input(id: "name", placeholder: "Your name")
      end +
      tag.div(class: "grid gap-1.5") do
        sui.label("Email", for: "email") +
        sui.input(id: "email", type: "email", placeholder: "[email protected]")
      end
    end
    sui.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
  sui.dialog_trigger "Edit profile", variant: :outline
  sui.dialog_content(src: edit_profile_path) do
    tag.p "Loading...", class: "text-sm text-muted-foreground animate-pulse p-4"
  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.

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
  sui.dialog_trigger "Edit profile", variant: :outline
  sui.dialog_content(src: edit_profile_path, reload: true) do
    tag.p "Loading...", class: "text-sm text-muted-foreground animate-pulse p-4"
  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).

# app/controllers/charts_controller.rb
def destroy
  if !params[:confirm] && @chart.has_active_users?
    render turbo_stream: turbo_stream.append(
      "shadcnrb-dialogs",
      partial: "confirmation", locals: { chart: @chart }
    )
  else
    @chart.destroy
    render turbo_stream: toast_stream("Chart deleted")
  end
end

API

Method Key args Default Description
dialog open: false Root container; set open: true to start in open state
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