Rapid Rails UI Pro

Upgrade to unlock this component

Get Pro →

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-action attributes

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.

Edit Profile

Update your profile information below. Click the X or backdrop to close.

Basic Modal
<%= 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)

Center Modal

This is a centered modal dialog.

Right Drawer

Settings

Left Drawer

Navigation

Top Sheet

Notifications

Bottom Sheet

Quick Actions

Position Examples
<%# 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

Small Modal

This is a small modal dialog.

Large Modal

This is a large modal dialog with more room for content.

Extra Large Modal

This is an extra large modal dialog for complex forms or content.

Size Options
<%= 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.

Edit Profile

This dialog is a centered modal on desktop, but slides up as a bottom sheet on mobile.

Try it: Resize your browser window below 768px width to see the responsive transformation.
Responsive Modal
<%= 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.

Settings

Notifications

Theme

Light mode
Dark mode
System default
Responsive Drawer
<%# 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.

Confirm Action

This dialog switches to sheet mode at 1024px instead of the default 768px.

Custom 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:

Important Notice

This dialog stays centered even on mobile instead of becoming a bottom sheet.

Notification

This dialog becomes a top sheet on mobile instead of the default bottom sheet.

Custom 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 %>

Confirm Delete

Are you sure you want to delete this item? This action cannot be undone.

Custom Header

JD

Jane Doe

Edit Profile

Profile editing form would go here.

Slots Usage
<%= 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.

Edit Profile

Tell us about yourself

Form Dialog
<%= 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

Delete Account?

This will permanently delete your account and all associated data. This action cannot be undone.

Success Confirmation

Payment Successful!

Your payment of $99.00 has been processed successfully.

Confirmation Dialog
<%= 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

  1. Add a Turbo Frame in your layout to receive dialog content
  2. Use data-turbo-frame="dialog" on links to open content in the dialog
  3. Wrap your view content in rui_dialog(turbo_frame: "dialog")
  4. 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 dialog
  • dialog.open - Boolean property to check if open

Backdrop Options

Choose from four backdrop styles.

Default Backdrop

Standard semi-transparent backdrop (50% opacity).

Dark Backdrop

Darker backdrop for more focus (75% opacity).

Blur Backdrop

Blurred backdrop effect with lighter opacity.

Light Backdrop

Lighter, more subtle backdrop (25% opacity).

Backdrop Options
<%# 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.

Required Form

Clicking the backdrop won't close this dialog. Use the close button.

No Close Button

Hide the X button in the header.

Complete This Action

Complete the action to close this dialog.

Behavior Options
<%# 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.

Filter Products

Price Range

-

Category

Rating

4 stars & up
3 stars & up
Any rating

Image Preview

Use a large modal for image previews or media galleries.

Mountain Landscape

Photo by John Doe

Mountain
Mobile Navigation Drawer
<%= 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.

Shopping Cart

Watch

Classic Watch

Silver • 1x

$199.00

Headphones

Wireless Headphones

Black • 2x

$298.00

Subtotal $497.00
Shipping $9.99
Total $506.99
Shopping Cart Drawer
<%= 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.

Cookie Preferences

We use cookies to enhance your experience. Choose which cookies you allow:

Required for the website to function

Help us understand how you use our site

Used to deliver personalized ads

Cookie Consent
<%= 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.

Quick View

Sneaker
New Arrival

Air Max Sneakers

★★★★★
(128 reviews)
$149.99

Classic design meets modern comfort. Perfect for everyday wear with premium cushioning technology.

Quick View Product
<%= 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.

Notifications

JD

John Doe commented on your post

2 minutes ago

Your order has been shipped!

1 hour ago

AS

Alice Smith started following you

3 hours ago

SY

System update completed

Yesterday

View All
Notification Center
<%= 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 by showModal()
  • aria-labelledby links to dialog title
  • aria-describedby links to dialog description (optional)
  • role="dialog" implicit on native <dialog> element

Keyboard Navigation

  • Escape closes the dialog (fires cancel event)
  • Tab cycles through focusable elements within dialog
  • Shift + Tab cycles 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-labelledby reference
  • 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
Using Stimulus Actions
<%# 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 }
Listening to Events
<%# 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:

Programmatic Control
// 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

Related Components