Dialog

Dialog component for modal and drawer overlays, built on the native <dialog> element. Any number of dialogs can be opened on top of each other.

Dialog Title

Centered by default

This is a centered dialog. Open another one from inside it — dialogs stack.

Nested Dialog

Anchored to the right, opened on top of the first dialog — only the bottom-most dialog dims the page.

Usage

# <% Async dialog (loads content via Turbo Frame) %>
<%= ui.btn "Edit", url: edit_post_path(post), data: { turbo_frame: :dialog } %>

# <% Sync dialog (content already on page) %>
<%= ui.btn "Open Dialog", data: { action: "click->dialogs#open", id: "myDialog" } %>
<%= ui.dialog(title: "Title", id: "myDialog") do %>
  <%= ui.dialog_body do %>
    Content here
  <% end %>
<% end %>
Props
PropTypeDefaultDescription
titleStringnilDialog title displayed in the header.
subtitleStringnilDialog subtitle displayed below the title.
positionSymbol
:center | :left | :right | :top | :bottom
:centerWhere the dialog is anchored. ":center" behaves like a modal; the edge positions behave like a drawer sliding in from that edge.
sizeString
"sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | "6xl"
"2xl"Controls width for ":center"/":left"/":right", height for ":top"/":bottom".
idStringnilUnique identifier for sync dialogs. Required for dialogs triggered by `dialogs#open`.
closableBooleantrueWhether to render the × close button in the header.
dismissibleBooleantrueWhether Esc and a backdrop click close the dialog. The close button always closes it regardless of this setting.
swipeBooleantrueWhether touch swipe-to-dismiss is enabled. Only applies to edge positions (":left", ":right", ":top", ":bottom") — ignored for ":center".
SubcomponentsUse subcomponents below or any HTML.
NameHelperDescription
dialog_headerui.dialog_headerHeader section with title, subtitle, and close button. Automatically rendered when title/subtitle props are provided.
dialog_bodyui.dialog_bodyScrollable content area of the dialog.
dialog_footerui.dialog_footerFooter section typically containing action buttons.

Examples

Async Dialog

<%= ui.btn "Async Dialog", url: privacy_path, data: { turbo_frame: :dialog } %>

Sync Dialog

Dialog Title

Dialog Subtitle

Dialog content

<%= ui.btn "Open Dialog", data: { action: "click->dialogs#open", id: "dialog1" } %>

<%= ui.dialog(title: "Dialog Title", subtitle: "Dialog Subtitle", id: "dialog1") do %>
  <%= ui.dialog_body do %>
    <p>Dialog content</p>
  <% end %>
<% end %>

Positions

Center

Default position.

Left

Slides in from the left.

Right

Slides in from the right.

Top

Slides in from the top.

Bottom

Slides in from the bottom.
<div class="flex flex-wrap gap-2">
  <%= ui.btn "Center", data: { action: "click->dialogs#open", id: "posCenter" } %>
  <%= ui.btn "Left", variant: :secondary, data: { action: "click->dialogs#open", id: "posLeft" } %>
  <%= ui.btn "Right", variant: :secondary, data: { action: "click->dialogs#open", id: "posRight" } %>
  <%= ui.btn "Top", variant: :secondary, data: { action: "click->dialogs#open", id: "posTop" } %>
  <%= ui.btn "Bottom", variant: :secondary, data: { action: "click->dialogs#open", id: "posBottom" } %>
</div>

<%= ui.dialog(title: "Center", id: "posCenter") { ui.dialog_body { "Default position." } } %>
<%= ui.dialog(title: "Left", position: :left, id: "posLeft") { ui.dialog_body { "Slides in from the left." } } %>
<%= ui.dialog(title: "Right", position: :right, id: "posRight") { ui.dialog_body { "Slides in from the right." } } %>
<%= ui.dialog(title: "Top", position: :top, size: :sm, id: "posTop") { ui.dialog_body { "Slides in from the top." } } %>
<%= ui.dialog(title: "Bottom", position: :bottom, size: :sm, id: "posBottom") { ui.dialog_body { "Slides in from the bottom." } } %>

Nested Dialogs

Level 1

Level 2

Level 3

As deep as you like.
<%= ui.btn "Open first dialog", data: { action: "click->dialogs#open", id: "nested1" } %>

<%= ui.dialog(title: "Level 1", id: "nested1") do %>
  <%= ui.dialog_body do %>
    <%= ui.btn "Open second dialog", variant: :secondary, data: { action: "click->dialogs#open", id: "nested2" } %>
  <% end %>
<% end %>

<%= ui.dialog(title: "Level 2", position: :right, id: "nested2") do %>
  <%= ui.dialog_body do %>
    <%= ui.btn "Open third dialog", variant: :secondary, data: { action: "click->dialogs#open", id: "nested3" } %>
  <% end %>
<% end %>

<%= ui.dialog(title: "Level 3", position: :bottom, size: :sm, id: "nested3") do %>
  <%= ui.dialog_body { "As deep as you like." } %>
<% end %>

Not Dismissible

Confirm your action

This dialog only closes via the button below.
<%= ui.btn "Open", data: { action: "click->dialogs#open", id: "notDismissible" } %>

<%= ui.dialog(title: "Confirm your action", dismissible: false, id: "notDismissible") do %>
  <%= ui.dialog_body { "This dialog only closes via the button below." } %>
  <%= ui.dialog_footer justify: :end do %>
    <%= ui.btn "Close", data: { action: "click->dialog#close" } %>
  <% end %>
<% end %>

Are you absolutely sure?

This cannot be undone.