Dialog
Modal and drawer component using native HTML <dialog> element. A unified API for both centered modals and edge-positioned drawers.
Key Features
- Native <dialog> - Browser handles focus trapping, Escape key, and accessibility
- Modal Mode - Centered dialogs for confirmations, forms, focused tasks
- Drawer Mode - Edge-positioned panels for settings, navigation, filters
- Responsive - Auto-transform modal to bottom sheet on mobile
- Turbo Integration - Auto-open on frame load, auto-close on form success
- CSS-Only Animations - Smooth entry/exit using @starting-style
- Slots - Header, body, and footer for flexible layouts
- Backdrop Options - Default, dark, blur, and light variants
- Full Accessibility - aria-modal, aria-labelledby, keyboard navigation
- Dark Mode - Full dark mode support
- Accessibility - aria-modal, aria-labelledby, keyboard navigation
- JavaScript API - Programmatic control with
data-actionattributes
Modal vs Drawer
Use Modal (center) when:
- Demanding immediate attention
- Confirmations and alerts
- Short, focused tasks
- Payment flows
Use Drawer (edges) when:
- Settings and preferences
- Navigation menus (mobile)
- Filters and search
- Chat or notifications
Basic Usage
Create a simple modal dialog with a title and content.
<%= rui_dialog(id: "my-dialog", title: "Edit Profile") do |d| %>
<% d.with_trigger(color: :primary) do %>
Open Modal
<% end %>
<% d.with_body do %>
<p>Update your profile information.</p>
<% end %>
<% end %>
Positions
The position parameter determines whether the dialog renders as a modal (center) or drawer (edges).
Center (Modal)
Right Drawer
Left Drawer
Top Sheet
Bottom Sheet
<%# Center Modal (default) %>
<%= rui_dialog(position: :center, title: "Modal") do |d| %>
<% d.with_trigger do %>Open Modal<% end %>
<% d.with_body do %>Content<% end %>
<% end %>
<%# Right Drawer - Settings, Filters %>
<%= rui_dialog(position: :right, title: "Settings", size: :lg) do |d| %>
<% d.with_trigger do %>Open Settings<% end %>
<% d.with_body do %>Settings form<% end %>
<% end %>
<%# Left Drawer - Navigation %>
<%= rui_dialog(position: :left, title: "Menu") do |d| %>
<% d.with_trigger do %>Menu<% end %>
<% d.with_body do %>Navigation links<% end %>
<% end %>
Sizes
Modal and drawer sizes differ based on position.
Modal (Center)
:sm- max-w-sm (384px):md- max-w-md (448px):lg- max-w-lg (512px):xl- max-w-xl (576px):2xl- max-w-2xl (672px):full- Full screen
Drawer (Edges)
:sm- w-72 (288px):md- w-80 (320px):lg- w-96 (384px):xl- w-[28rem] (448px):2xl- w-[32rem] (512px):full- Full width
<%= rui_dialog(title: "Confirm", size: :sm) do |d| %>
<% d.with_trigger do %>Open Small<% end %>
<% d.with_body do %>Are you sure?<% end %>
<% end %>
<%= rui_dialog(title: "Edit", size: :lg) do |d| %>
<% d.with_trigger do %>Open Large<% end %>
<% d.with_body do %>Form content<% end %>
<% end %>
<%= rui_dialog(title: "Preview", size: :xl) do |d| %>
<% d.with_trigger do %>Open XL<% end %>
<% d.with_body do %>Large content area<% end %>
<% end %>
Responsive Dialog
Enable responsive: true to automatically transform dialogs into mobile-friendly bottom sheets on smaller viewports. Perfect for touch-friendly UX.
How It Works
- Desktop (≥768px) - Shows as normal modal or drawer
- Mobile (<768px) - Transforms to full-width bottom sheet
- CSS-Only - No JavaScript layout thrashing, pure CSS media queries
- Auto-Adapt Size - Width becomes 100%, max-height 85vh on mobile
Position Mapping (Desktop → Mobile)
:center→:bottom(sheet):right→:bottom(sheet):left→:bottom(sheet):top→ stays:top:bottom→ stays:bottom
Responsive Modal
A centered modal that becomes a bottom sheet on mobile. Resize your browser to see the transformation.
<%= rui_dialog(title: "Edit Profile", responsive: true) do |d| %>
<% d.with_trigger(color: :violet) do %>
Responsive Modal
<% end %>
<% d.with_body do %>
<p>This dialog transforms to a bottom sheet on mobile.</p>
<% end %>
<% d.with_footer do %>
<%= rui_button("Cancel", variant: :outline, data: { action: "dialog#close" }) %>
<%= rui_button("Save", color: :primary) %>
<% end %>
<% end %>
Responsive Drawer
A right drawer that slides in from the right on desktop but becomes a bottom sheet on mobile.
<%# Right drawer on desktop, bottom sheet on mobile %>
<%= rui_dialog(position: :right, title: "Settings", size: :lg, responsive: true) do |d| %>
<% d.with_trigger do |btn| %>
<% btn.with_icon(:settings) %>
Settings
<% end %>
<% d.with_body do %>
<!-- Settings content -->
<% end %>
<% d.with_footer do %>
<%= rui_button("Cancel", variant: :outline, data: { action: "dialog#close" }) %>
<%= rui_button("Save Settings", color: :primary) %>
<% end %>
<% end %>
Custom Breakpoint
Adjust when the responsive transformation happens with responsive_breakpoint.
<%# Switch at tablet size (1024px) instead of 768px %>
<%= rui_dialog(
title: "Confirm Action",
responsive: true,
responsive_breakpoint: 1024
) do |d| %>
<% d.with_body do %>
<p>This switches to sheet mode at 1024px.</p>
<% end %>
<% d.with_footer do %>
<%= rui_button("Cancel", variant: :outline, data: { action: "dialog#close" }) %>
<%= rui_button("Confirm", color: :success) %>
<% end %>
<% end %>
<%# Common breakpoint values %>
<%# 640 - sm (Tailwind mobile) %>
<%# 768 - md (default, tablet portrait) %>
<%# 1024 - lg (tablet landscape) %>
<%# 1280 - xl (small desktop) %>
Responsive Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
responsive |
Boolean | false |
Enable responsive position switching |
responsive_breakpoint |
Integer | 768 |
Breakpoint in pixels for responsive switch |
mobile_position |
Symbol | auto | Override mobile position (:bottom, :top, :center) |
Custom Mobile Position
By default, mobile_position is auto-determined based on the desktop position:
| Desktop Position | Default Mobile Position |
|---|---|
:center | :bottom (sheet) |
:right | :bottom (sheet) |
:left | :bottom (sheet) |
:top | :top (stays) |
:bottom | :bottom (stays) |
Override the default with mobile_position:
<%# Keep centered on mobile instead of bottom sheet %>
<%= rui_dialog(
title: "Important Notice",
responsive: true,
mobile_position: :center
) do |d| %>
<% d.with_body do %>
This stays centered even on mobile.
<% end %>
<% end %>
<%# Use top sheet on mobile %>
<%= rui_dialog(
title: "Notification",
responsive: true,
mobile_position: :top
) do |d| %>
<% d.with_body do %>
This becomes a top sheet on mobile.
<% end %>
<% end %>
Why Responsive Dialogs? Bottom sheets are more thumb-friendly on mobile devices. They slide up from the bottom, matching platform conventions (iOS/Android action sheets) and making better use of narrow screens.
Slots
Use header, body, and footer slots for structured layouts.
<%= rui_dialog(title: "Confirm Delete") do |d| %>
<% d.with_body do %>
Are you sure you want to delete this item?
<% end %>
<% d.with_footer do %>
<%= rui_button("Cancel", variant: :outline, data: { action: "dialog#close" }) %>
<%= rui_button("Delete", color: :danger) %>
<% end %>
<% end %>
Custom Header
<%= rui_dialog(id: "confirm-dialog", title: "Confirm Delete") do |d| %>
<% d.with_body do %>
<p>Are you sure you want to delete this item?</p>
<% end %>
<% d.with_footer do %>
<%= rui_button("Cancel", variant: :outline, data: { action: "dialog#close" }) %>
<%= rui_button("Delete", color: :danger) %>
<% end %>
<% end %>
<%# Custom header with avatar %>
<%= rui_dialog(id: "user-dialog") do |d| %>
<% d.with_header do %>
<div class="flex items-center gap-3">
<%= rui_avatar(initials: "JD", color: :blue) %>
<div>
<h2 class="font-semibold">Jane Doe</h2>
<p class="text-sm text-zinc-500">Edit Profile</p>
</div>
</div>
<% end %>
<% d.with_body do %>
<!-- Form content -->
<% end %>
<% end %>
Form Dialogs
Dialogs are perfect for inline editing forms. Combine with Rails forms for a seamless experience.
<%= rui_dialog(title: "Edit Profile", size: :lg) do |d| %>
<% d.with_body do %>
<form class="space-y-4">
<%= rui_input(name: :email, label: "Email", type: :email) %>
<%= rui_textarea(name: :bio, label: "Bio") %>
</form>
<% end %>
<% d.with_footer do %>
<%= rui_button("Cancel", variant: :outline, data: { action: "dialog#close" }) %>
<%= rui_button("Save", color: :primary) %>
<% end %>
<% end %>
Tip: Use data: { action: "dialog#close" } on cancel buttons to close the dialog without submitting the form.
Confirmation Dialogs
Create focused confirmation dialogs for destructive actions or important decisions.
Delete Confirmation
Success Confirmation
<%= rui_dialog(title: "Delete Account?", size: :sm, backdrop: :dark) do |d| %>
<% d.with_body do %>
<div class="text-center">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4">
<!-- Warning icon -->
</div>
<p>This will permanently delete your account.</p>
</div>
<% end %>
<% d.with_footer do %>
<%= rui_button("Cancel", variant: :outline, data: { action: "dialog#close" }) %>
<%= rui_button("Delete", color: :danger) %>
<% end %>
<% end %>
Turbo Integration
The Dialog component integrates seamlessly with Turbo for dynamic content loading.
How It Works
- Add a Turbo Frame in your layout to receive dialog content
- Use
data-turbo-frame="dialog"on links to open content in the dialog - Wrap your view content in
rui_dialog(turbo_frame: "dialog") - Forms auto-close the dialog on success, stay open on validation errors
Layout Setup
<%# app/views/layouts/application.html.erb %>
<body>
<%= yield %>
<%# Add dialog container at end of body %>
<%= rui_dialog(id: "dialog", turbo_frame: "dialog") %>
</body>
Link to Open Dialog
<%= link_to "Edit Post", edit_post_path(@post),
data: { turbo_frame: "dialog" } %>
View Content
<%# app/views/posts/edit.html.erb %>
<%= rui_dialog(title: "Edit Post", turbo_frame: "dialog") do %>
<%= render "form", post: @post %>
<% end %>
Static Dialogs
Static dialogs are always in the DOM and can be opened using the with_trigger slot.
<%= rui_dialog(id: "my-dialog", title: "Static Dialog") do |d| %>
<% d.with_trigger do %>
Open Dialog
<% end %>
<% d.with_body do %>
This dialog is always in the DOM.
<% end %>
<% end %>
Native Dialog Methods
The trigger slot uses Stimulus to call native dialog methods:
dialog.showModal()- Opens as modal (focus trap, backdrop, Escape)dialog.close()- Closes the dialogdialog.open- Boolean property to check if open
Backdrop Options
Choose from four backdrop styles.
<%# Default - 50% opacity (standard) %>
<%= rui_dialog(title: "Modal", backdrop: :default) do |d| %>
<% d.with_body do %>Standard backdrop<% end %>
<% end %>
<%# Dark - 75% opacity (more focus) %>
<%= rui_dialog(title: "Important", backdrop: :dark) do |d| %>
<% d.with_body do %>Darker overlay<% end %>
<% end %>
<%# Light - 25% opacity (subtle) %>
<%= rui_dialog(title: "Info", backdrop: :light) do |d| %>
<% d.with_body do %>Subtle overlay<% end %>
<% end %>
<%# Blur - 30% opacity + blur effect %>
<%= rui_dialog(title: "Premium", backdrop: :blur) do |d| %>
<% d.with_body do %>Frosted glass effect<% end %>
<% end %>
Behavior Options
Non-Dismissible
Prevent closing when clicking the backdrop.
No Close Button
Hide the X button in the header.
<%# Non-dismissible - Backdrop click does not close %>
<%= rui_dialog(title: "Required Form", dismissible: false) do |d| %>
<% d.with_body do %>
Must use close button or complete action
<% end %>
<% end %>
<%# No close button - Hide the X button %>
<%= rui_dialog(title: "Wizard Step", closable: false) do |d| %>
<% d.with_body do %>
Complete this step to continue
<% end %>
<% d.with_footer do %>
<%= rui_button("Complete", data: { action: "dialog#close" }) %>
<% end %>
<% end %>
<%# Forced interaction - Both options disabled %>
<%= rui_dialog(
title: "Terms & Conditions",
closable: false,
dismissible: false
) do |d| %>
<% d.with_body do %>
Please read and accept the terms
<% end %>
<% d.with_footer do %>
<%= rui_button("Accept", data: { action: "dialog#close" }) %>
<% end %>
<% end %>
Common Use Cases
Mobile Navigation
Use a left drawer for mobile navigation menus.
Filters Panel
Use a right drawer for filter panels in e-commerce or data tables.
Image Preview
Use a large modal for image previews or media galleries.
<%= rui_dialog(position: :left, title: "Menu", size: :sm) do |d| %>
<% d.with_body do %>
<nav class="space-y-1">
<%= link_to "Home", root_path, class: "..." %>
<%= link_to "Products", products_path, class: "..." %>
<%= link_to "About", about_path, class: "..." %>
</nav>
<% end %>
<% end %>
Shopping Cart
A right drawer for shopping cart preview with RUI components.
<%= rui_dialog(position: :right, title: "Shopping Cart", size: :md) do |d| %>
<% d.with_body do %>
<% @cart_items.each do |item| %>
<div class="flex gap-4 pb-4 border-b">
<%= image_tag item.image, class: "w-16 h-16 rounded-lg object-cover" %>
<div class="flex-1">
<h4 class="font-medium"><%= item.name %></h4>
<p class="text-sm text-zinc-500"><%= item.variant %> • <%= item.quantity %>x</p>
<p class="font-semibold"><%= number_to_currency(item.price) %></p>
</div>
<%= rui_button(variant: :ghost, size: :sm, class: "text-red-500") do |btn| %>
<% btn.with_icon(:trash_2) %>
<% end %>
</div>
<% end %>
<% end %>
<% d.with_footer do %>
<%= rui_button("Continue Shopping", variant: :outline, data: { action: "dialog#close" }) %>
<%= rui_button("Checkout", color: :primary) %>
<% end %>
<% end %>
Cookie Consent
A bottom sheet for cookie consent with checkboxes.
<%= rui_dialog(position: :bottom, closable: false, dismissible: false) do |d| %>
<% d.with_header do %>
<div class="flex items-center gap-3">
<!-- Cookie icon -->
<h2 class="font-semibold">Cookie Preferences</h2>
</div>
<% end %>
<% d.with_body do %>
<p>We use cookies to enhance your experience.</p>
<div class="space-y-3">
<%= rui_checkbox(:essential, label: "Essential", checked: true, disabled: true) %>
<%= rui_checkbox(:analytics, label: "Analytics") %>
<%= rui_checkbox(:marketing, label: "Marketing") %>
</div>
<% end %>
<% d.with_footer do %>
<%= rui_button("Reject All", variant: :outline, data: { action: "dialog#close" }) %>
<%= rui_button("Accept Selected", color: :primary, data: { action: "dialog#close" }) %>
<% end %>
<% end %>
Quick View Product
A large modal for quick product preview without leaving the page.
<%= rui_dialog(title: "Quick View", size: :xl) do |d| %>
<% d.with_body do %>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<%= image_tag @product.image, class: "w-full rounded-lg" %>
</div>
<div class="space-y-4">
<%= rui_badge("New Arrival", color: :green, size: :sm) %>
<h3 class="text-2xl font-bold"><%= @product.name %></h3>
<div class="text-3xl font-bold"><%= number_to_currency(@product.price) %></div>
<p><%= @product.description %></p>
<!-- Size and color selectors -->
</div>
</div>
<% end %>
<% d.with_footer do %>
<%= rui_button("View Full Details", variant: :outline) %>
<%= rui_button("Add to Cart", color: :primary) do |btn| %>
<% btn.with_icon(:shopping_cart) %>
<% end %>
<% end %>
<% end %>
Notification Center
A right drawer for viewing notifications.
<%= rui_dialog(position: :right, title: "Notifications", size: :md) do |d| %>
<% d.with_body do %>
<% @notifications.each do |notification| %>
<div class="p-3 rounded-lg border-l-4 border-<%= notification.color %>-500">
<div class="flex items-start gap-3">
<%= rui_avatar(initials: notification.user_initials, size: :sm) %>
<div class="flex-1">
<p class="font-medium"><%= notification.message %></p>
<p class="text-sm text-zinc-500"><%= time_ago_in_words(notification.created_at) %> ago</p>
</div>
</div>
</div>
<% end %>
<% end %>
<% d.with_footer do %>
<%= rui_button("Mark All Read", variant: :ghost, size: :sm) %>
<%= rui_link("View All", href: notifications_path) %>
<% end %>
<% end %>
Accessibility
The Dialog component uses the native HTML <dialog> element for built-in accessibility.
ARIA Attributes
-
aria-modal="true"automatically set byshowModal() -
aria-labelledbylinks to dialog title -
aria-describedbylinks to dialog description (optional) -
role="dialog"implicit on native<dialog>element
Keyboard Navigation
-
Escapecloses the dialog (firescancelevent) -
Tabcycles through focusable elements within dialog -
Shift + Tabcycles backwards through elements
Semantic HTML
-
Native
<dialog>element with built-in modal behavior -
Focus trapping handled automatically by
showModal() - Background made inert (non-interactive) when modal is open
- Focus restoration to trigger element when dialog closes
Screen Reader Support
- Dialog announced as "modal dialog" when opened
-
Title read aloud from
aria-labelledbyreference - Content outside dialog is ignored while modal is open
JavaScript API
The Dialog component exposes Stimulus actions and custom events for programmatic control.
Stimulus Actions
Use these actions via data-action attributes:
| Action | Description |
|---|---|
dialog#open |
Opens the dialog as a modal |
dialog#close |
Closes the dialog |
dialog#toggle |
Toggles open/close state |
<%# Open dialog from any button %>
<button data-action="dialog#open" data-dialog-target="trigger">
Open
</button>
<%# Close from inside dialog %>
<%= rui_button("Cancel", variant: :outline, data: { action: "dialog#close" }) %>
<%# Toggle behavior %>
<button data-action="dialog#toggle">Toggle Dialog</button>
Custom Events
Listen to these events for lifecycle hooks:
| Event | When | Detail |
|---|---|---|
dialog:open |
Dialog is opened | { dialog } |
dialog:closed |
Dialog is closed | { dialog, returnValue } |
dialog:cancel |
Escape key pressed | { dialog } |
dialog:backdropClick |
Backdrop clicked | { dialog } |
<%# Listen for dialog close %>
<div data-controller="my-controller"
data-action="dialog:closed->my-controller#handleClose">
<%= rui_dialog(title: "Example") do |d| %>
<% d.with_body do %>Content<% end %>
<% end %>
</div>
<%# In your Stimulus controller %>
// my_controller.js
handleClose(event) {
const { dialog, returnValue } = event.detail
console.log("Dialog closed with:", returnValue)
}
Programmatic Control
Control the dialog from JavaScript using Stimulus outlet or direct DOM access:
// Get the dialog controller
const dialogElement = document.querySelector('[data-controller="dialog"]')
const dialogController = application.getControllerForElementAndIdentifier(
dialogElement,
"dialog"
)
// Open/close programmatically
dialogController.open()
dialogController.close()
// Or use native dialog methods directly
const dialog = document.getElementById("my-dialog")
dialog.showModal() // Opens as modal
dialog.close() // Closes dialog
API Reference
rui_dialog
Modal and drawer component using native HTML <dialog> element
| Parameter | Type | Default | Description |
|---|---|---|---|
| id | String |
auto-generated
|
Unique ID for the dialog |
| title | String | — | Title text for the header |
Position
Position determines modal vs drawer mode
| Parameter | Type | Default | Description |
|---|---|---|---|
| position | Symbol |
:center
|
Position of dialog
:center
:right
:left
:top
:bottom
|
Appearance
Visual styling options
| Parameter | Type | Default | Description |
|---|---|---|---|
| size | Symbol |
:md
|
Size variant
:sm
:md
:lg
:xl
:2xl
:full
|
| backdrop | Symbol |
:default
|
Backdrop style
:default
:dark
:blur
:light
|
Behavior
Interaction options
| Parameter | Type | Default | Description |
|---|---|---|---|
| closable | Boolean |
true
|
Show close button in header |
| dismissible | Boolean |
true
|
Close on backdrop click |
| static_backdrop | Boolean |
false
|
Prevent Escape key from closing |
| open | Boolean |
false
|
Whether dialog starts open |
Turbo Integration
Options for Turbo Frame integration
| Parameter | Type | Default | Description |
|---|---|---|---|
| turbo_frame | String | — | Turbo Frame ID for dynamic loading |
| autofocus_selector | String | — | CSS selector for element to focus on open |
Slots
Content slots for customizing component parts
| Slot | Description |
|---|---|
| header | Custom header content (replaces default title) |
| body | Main content area |
| footer | Action buttons area |