Turbo Modal System Usage Guide
Complete guide for implementing accessible, responsive modals using Hotwire Turbo, Stimulus.js, and Tailwind CSS 4.0.
Table of Contents
- Overview
- Quick Start
- Implementation Guide
- Modal Configuration
- Form Integration
- Stimulus Controller Features
- Accessibility & UX
- Troubleshooting
- Advanced Usage
Overview
๐ Modern Modal System
A sophisticated modal implementation leveraging the power of Hotwire Turbo with comprehensive accessibility and modern UX patterns.
Core Features
| Feature | Description | Benefit |
|---|---|---|
| Keyboard Navigation | ESC to close, Tab trapping | Full accessibility support |
| Focus Management | Automatic focus and restoration | Screen reader friendly |
| Dark Mode Support | Complete dark theme integration | Modern UI standards |
| Responsive Design | Mobile-optimized layouts | Cross-device compatibility |
| Smooth Animations | CSS transition animations | Professional UX |
| WCAG 2.2 AA Compliant | Full accessibility compliance | Inclusive design |
Technology Stack
โก Modern Architecture
Built on cutting-edge web technologies for optimal performance and developer experience.
- ๐๏ธ Hotwire Turbo - Server-side rendering with SPA-like experience
- โก Stimulus.js - JavaScript behaviors and interactions
- ๐จ Tailwind CSS 4.0 - Utility-first styling system
- โฟ Accessibility First - WCAG 2.2 AA compliant implementation
Quick Start
โก Get Started in Minutes
Follow these essential steps to implement your first Turbo modal.
Essential Components
1. Controller Action Setup
๐ฎ Server Response Configuration
Configure your Rails controller to respond with modal content using Turbo Streams.
def edit
respond_to do |format|
format.html # Regular edit page
format.turbo_stream do
render turbo_stream: turbo_stream.replace(:modal,
partial: "shared/turbo_modal",
locals: {
modal_title: "Edit #{@organization.name}",
modal_size: "lg"
}) do
render partial: "edit_form", locals: { organization: @organization }
end
end
end
end
2. Link Configuration
๐ Trigger Setup
Configure links to properly target the modal frame for seamless integration.
<%= link_to "Edit", edit_organization_path(organization),
data: {
turbo_frame: "modal",
turbo_method: :get
} %>
3. Form Integration
๐ Form Configuration
Set up forms to work properly within the modal context.
<%= form_with(model: organization,
local: false,
data: { turbo_frame: "_top" }) do |form| %>
<!-- form fields -->
<% end %>
Implementation Guide
Step 1: Controller Setup
๐ฏ Turbo Stream Integration
Configure your controllers to respond with modal content using Turbo Streams.
Basic Modal Response Pattern
๐ง Standard Implementation
The fundamental pattern for rendering modal content via Turbo Streams.
class OrganizationsController < ApplicationController
def edit
respond_to do |format|
format.html # Fallback for direct access
format.turbo_stream do
render turbo_stream: turbo_stream.replace(:modal,
partial: "shared/turbo_modal",
locals: modal_locals
) do
render partial: "organizations/edit_form",
locals: { organization: @organization }
end
end
end
end
private
def modal_locals
{
modal_title: "Edit #{@organization.name}",
modal_size: "lg",
modal_id: "edit-organization-modal"
}
end
end
Advanced Response Handling
โก Multi-Stream Responses
Advanced pattern for updating multiple page elements simultaneously.
def show
respond_to do |format|
format.html
format.turbo_stream do
render turbo_stream: [
turbo_stream.replace(:modal,
partial: "shared/turbo_modal",
locals: {
modal_title: @organization.name,
modal_size: "xl"
}
) do
render partial: "organizations/details",
locals: { organization: @organization }
end,
turbo_stream.append(:page_history,
partial: "shared/history_entry",
locals: { action: "viewed", resource: @organization }
)
]
end
end
end
Step 2: Link and Button Setup
๐ Trigger Configuration
Configure links and buttons to properly target the modal frame.
Basic Modal Triggers
๐ฏ Standard Trigger Patterns
Common patterns for triggering modal display from various UI elements.
<!-- Edit button -->
<%= link_to "Edit Organization",
edit_organization_path(organization),
class: "btn btn-primary",
data: {
turbo_frame: "modal",
turbo_method: :get
} %>
<!-- Delete confirmation modal -->
<%= link_to "Delete",
organization_path(organization),
class: "btn btn-danger",
data: {
turbo_frame: "modal",
turbo_method: :delete,
turbo_confirm: "Are you sure?"
} %>
Button with Loading States
๐ Enhanced UX Patterns
Implement loading states for better user feedback during modal loading.
<%= link_to edit_organization_path(organization),
class: "btn btn-primary",
data: {
turbo_frame: "modal",
turbo_method: :get,
action: "click->loading#start"
} do %>
<svg class="size-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
Edit Organization
<% end %>
Modal Configuration
Available Modal Sizes
๐ Responsive Sizing Options
Choose the appropriate modal size based on your content and use case.
| Size Parameter | CSS Classes | Viewport Width | Best Use Case |
|---|---|---|---|
sm or small |
max-w-sm |
~384px | Confirmations, alerts |
md (default) |
max-w-md |
~448px | Simple forms |
lg or large |
max-w-2xl |
~672px | Complex forms |
xl or extra-large |
max-w-4xl |
~896px | Data tables, dashboards |
full |
max-w-7xl |
~1280px | Full-screen experiences |
Size Configuration Examples
๐จ Size Implementation
Practical examples of different modal sizes for various use cases.
<!-- Small confirmation modal -->
<%= render 'shared/turbo_modal',
modal_title: "Confirm Action",
modal_size: "sm" do %>
<p>Are you sure you want to proceed?</p>
<% end %>
<!-- Large form modal -->
<%= render 'shared/turbo_modal',
modal_title: "Edit Organization",
modal_size: "lg" do %>
<%= render 'organizations/edit_form' %>
<% end %>
<!-- Extra-large data modal -->
<%= render 'shared/turbo_modal',
modal_title: "Analytics Dashboard",
modal_size: "xl" do %>
<%= render 'analytics/dashboard' %>
<% end %>
Modal Helper Configuration
๐ ๏ธ Helper Method Integration
Utilize the modal size helper for consistent styling across your application.
# app/helpers/application_helper.rb
def modal_size_classes(size = nil)
case size&.to_s
when 'xs', 'extra-small'
'max-w-xs'
when 'sm', 'small'
'max-w-sm'
when 'lg', 'large'
'max-w-2xl'
when 'xl', 'extra-large'
'max-w-4xl'
when '2xl', 'extra-extra-large'
'max-w-6xl'
when 'full'
'max-w-7xl'
else
'max-w-md' # default medium size
end
end
Form Integration
Form Configuration for Modals
๐ Proper Form Setup
Configure forms to work seamlessly with the modal system and Turbo.
Standard Form Pattern
๐ฏ Complete Form Implementation
Comprehensive form pattern optimized for modal usage with proper styling and accessibility.
<%= form_with(model: [@organization, @project],
local: false,
data: { turbo_frame: "_top" },
class: "space-y-4") do |form| %>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="sm:col-span-2">
<%= form.label :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :name,
class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:text-white" %>
</div>
<div>
<%= form.label :status, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.select :status,
options_for_select([['Active', 'active'], ['Inactive', 'inactive']]),
{ prompt: 'Select status' },
{ class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:text-white" } %>
</div>
<div>
<%= form.label :priority, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.number_field :priority,
in: 1..5,
class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:text-white" %>
</div>
</div>
<div class="flex justify-end space-x-2 pt-4 border-t border-gray-200 dark:border-gray-700">
<%= link_to "Cancel", "#",
class: "btn btn-secondary",
data: { action: "click->modal#closeModal" } %>
<%= form.submit "Save Changes",
class: "btn btn-primary" %>
</div>
<% end %>
Success Response Handling
โ Post-Form Processing
Handle successful form submissions with appropriate redirects or updates.
Option A: Redirect to Show Page (Recommended)
๐ Clean Redirect Pattern
The recommended approach for handling successful form submissions.
def update
if @organization.update(organization_params)
respond_to do |format|
format.turbo_stream do
redirect_to organization_path(@organization),
notice: "Organization was successfully updated."
end
end
else
handle_form_errors
end
end
Option B: Update in Place with Turbo Streams
โก In-Place Updates
Advanced pattern for updating content without full page reload.
def update
if @organization.update(organization_params)
respond_to do |format|
format.turbo_stream do
flash.now[:notice] = "Organization was successfully updated."
render turbo_stream: [
turbo_stream.remove(:modal),
turbo_stream.replace("organization_#{@organization.id}",
partial: "organization",
locals: { organization: @organization }),
turbo_stream.prepend("flash",
partial: "shared/flash")
]
end
end
else
handle_form_errors
end
end
Error Handling
๐จ Validation Error Management
Gracefully handle validation errors within the modal context.
def handle_form_errors
respond_to do |format|
format.turbo_stream do
render turbo_stream: turbo_stream.replace(:inside_modal,
partial: "edit_form",
locals: { organization: @organization }
), status: :unprocessable_entity
end
end
end
# Alternative: Update specific form sections
def handle_field_errors
respond_to do |format|
format.turbo_stream do
render turbo_stream: [
turbo_stream.replace("form_errors",
partial: "shared/form_errors",
locals: { object: @organization }),
turbo_stream.replace("form_fields",
partial: "organizations/form_fields",
locals: { form: form, organization: @organization })
], status: :unprocessable_entity
end
end
end
Stimulus Controller Features
Automatic Functionality
๐ค Built-in Behaviors
The modal Stimulus controller provides comprehensive functionality out of the box.
| Feature | Trigger | Description |
|---|---|---|
| Focus Management | Modal open | Automatically focuses first focusable element |
| Focus Trapping | Tab navigation | Keeps navigation within modal boundaries |
| Keyboard Navigation | ESC key | Closes modal instantly |
| Backdrop Close | Click outside | Closes modal when clicking backdrop |
| Body Scroll Lock | Modal open | Prevents background scrolling |
| Smooth Animations | Open/close | Scale and opacity transitions |
Controller Methods and Events
JavaScript Integration
โก Advanced Integration
Custom JavaScript patterns for complex modal interactions.
// Access modal controller from other Stimulus controllers
connect() {
this.modalElement = document.getElementById('modal')
this.modalController = this.application.getControllerForElementAndIdentifier(
this.modalElement,
"modal"
)
}
// Manually close modal
closeModal() {
if (this.modalController) {
this.modalController.closeModal()
}
}
// Check if modal is open
isModalOpen() {
return this.modalElement && !this.modalElement.classList.contains("hidden")
}
// Listen for modal events
modalOpened() {
// Custom logic when modal opens
console.log("Modal opened")
}
modalClosed() {
// Custom logic when modal closes
console.log("Modal closed")
}
Custom Event Handling
๐ก Event System Integration
Listen for and respond to modal lifecycle events.
<!-- Listen for modal events -->
<div data-controller="modal-listener"
data-action="modal:opened->modal-listener#handleModalOpened
modal:closed->modal-listener#handleModalClosed">
<!-- content -->
</div>
Accessibility & UX
Accessibility Features
โฟ WCAG 2.2 AA Compliance
Comprehensive accessibility support built into every modal component.
| Feature | Implementation | Benefit |
|---|---|---|
| Focus Management | Automatic focus on first element | Screen reader navigation |
| Focus Trapping | Tab/Shift+Tab cycling | Keyboard-only users |
| Keyboard Navigation | ESC key closing | Universal accessibility |
| ARIA Attributes | role="dialog", aria-modal="true" |
Screen reader context |
| Semantic HTML | Proper heading hierarchy | Document structure |
| High Contrast | Dark mode support | Visual accessibility |
ARIA Implementation
๐ฏ Accessibility Standards
Complete ARIA implementation ensuring proper screen reader support.
<!-- Modal with complete ARIA support -->
<div id="modal"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-description"
data-controller="modal"
data-action="keydown.esc->modal#closeModal click->modal#backdropClick"
class="fixed inset-0 z-50 overflow-y-auto"
style="display: none;">
<div class="flex min-h-screen items-center justify-center p-4">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full">
<header class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 id="modal-title" class="text-lg font-semibold text-gray-900 dark:text-white">
<%= modal_title %>
</h2>
</header>
<div id="modal-description" class="sr-only">
Modal dialog for <%= modal_title %>
</div>
<div class="p-6">
<%= yield %>
</div>
</div>
</div>
</div>
UX Best Practices
Loading States
๐ User Feedback Patterns
Provide clear feedback during loading and processing states.
<!-- Button with loading state -->
<%= link_to edit_path(resource),
data: { turbo_frame: "modal" },
class: "btn btn-primary" do %>
<span data-loading-text="Loading...">Edit</span>
<svg class="animate-spin size-4 ml-2 hidden" data-loading-spinner>
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/>
<path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
</svg>
<% end %>
Progressive Enhancement
๐ Graceful Degradation
Ensure functionality works for all users, regardless of JavaScript support.
<!-- Fallback for non-JavaScript users -->
<noscript>
<%= link_to "Edit (opens in new page)", edit_path(resource),
class: "btn btn-primary" %>
</noscript>
<!-- Enhanced version for JavaScript users -->
<div class="js-only">
<%= link_to "Edit", edit_path(resource),
data: { turbo_frame: "modal" },
class: "btn btn-primary" %>
</div>
Troubleshooting
Common Issues & Solutions
Modal Not Opening
๐จ Modal Display Issues
Diagnostic steps and solutions for modal display problems.
Symptoms:
- Link clicks don't show modal
- Page refreshes instead of modal
- JavaScript errors in console
Diagnostic Steps:
๐ Debugging Process
Systematic approach to identifying modal display issues.
<!-- Check Turbo Frame target -->
<%= link_to "Test", test_path,
data: {
turbo_frame: "modal",
turbo_method: :get
} %>
<!-- Verify controller response -->
def test
respond_to do |format|
format.turbo_stream do
render turbo_stream: turbo_stream.replace(:modal, "Modal content")
end
end
end
Solutions:
โ Resolution Steps
Step-by-step solutions for common modal opening issues.
- Verify Turbo Frame Target: Ensure
data: { turbo_frame: "modal" } - Check Controller Response: Must respond to
turbo_streamformat - Validate Modal Partial: Ensure
shared/turbo_modalexists - JavaScript Console: Check for Stimulus controller errors
Modal Not Closing
๐ Modal Dismissal Problems
Troubleshooting modal closing functionality.
Symptoms:
- ESC key doesn't work
- Backdrop clicks don't close modal
- Close button non-functional
Diagnostic Steps:
๐งช Testing Approach
Methods to verify modal closing functionality.
// Check modal controller connection
console.log(document.querySelector('[data-controller="modal"]'))
// Verify event listeners
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
console.log('ESC key detected')
}
})
Solutions:
๐ง Fix Implementation
Common solutions for modal closing issues.
- Verify Controller Connection: Check
data-controller="modal" - Check Event Bindings: Ensure
data-actionattributes are correct - Inspect Close Buttons: Verify
data-action="click->modal#closeModal" - JavaScript Errors: Check console for Stimulus errors
Focus Management Issues
๐ฏ Focus and Accessibility Problems
Resolving focus management and accessibility concerns.
Symptoms:
- No automatic focus on modal open
- Tab navigation escapes modal
- Screen reader issues
Solutions:
โฟ Accessibility Fixes
Ensuring proper focus management for all users.
<!-- Ensure focusable elements exist -->
<div class="modal-content">
<button type="button" class="sr-only" data-modal-target="firstFocusable">
First focusable element
</button>
<!-- Your modal content -->
<button type="button" class="sr-only" data-modal-target="lastFocusable">
Last focusable element
</button>
</div>
Styling and Animation Issues
๐จ Visual and Animation Problems
Resolving styling and animation-related issues.
Common Solutions:
๐ฌ Animation Fixes
Ensuring smooth animations and proper styling.
<!-- Ensure proper CSS classes -->
<div class="fixed inset-0 z-50 overflow-y-auto transition-opacity duration-300"
data-controller="modal"
data-modal-backdrop-classes="bg-gray-500 bg-opacity-50"
data-modal-container-classes="flex min-h-screen items-center justify-center p-4">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl transform transition-all duration-300 scale-95 opacity-0"
data-modal-target="container">
<!-- Modal content -->
</div>
</div>
Advanced Usage
Custom Modal Sizes
๐ Extended Size Options
Add custom modal sizes for specific use cases.
# app/helpers/application_helper.rb
def modal_size_classes(size = nil)
case size&.to_s
when 'xs', 'extra-small'
'max-w-xs'
when 'sm', 'small'
'max-w-sm'
when 'md', 'medium'
'max-w-md'
when 'lg', 'large'
'max-w-2xl'
when 'xl', 'extra-large'
'max-w-4xl'
when '2xl', 'extra-extra-large'
'max-w-6xl'
when '3xl', 'triple-large'
'max-w-7xl'
when 'full', 'fullscreen'
'max-w-none w-full h-full'
when 'custom-narrow'
'max-w-96'
when 'custom-wide'
'max-w-5xl'
else
'max-w-md'
end
end
Custom Modal Actions
๐ง Advanced Modal Functionality
Create modals with custom footer actions and behaviors.
<%= render 'shared/turbo_modal',
modal_title: "Confirm Dangerous Action",
modal_size: "sm" do %>
<div class="text-center p-6">
<div class="mx-auto flex items-center justify-center size-12 rounded-full bg-red-100 dark:bg-red-900 mb-4">
<svg class="size-6 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
</div>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">
Are you absolutely sure?
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
This action cannot be undone. This will permanently delete the organization and all associated data.
</p>
<div class="flex space-x-3 justify-center">
<button type="button"
class="btn btn-secondary"
data-action="click->modal#closeModal">
Cancel
</button>
<%= link_to "Yes, delete organization",
organization_path(@organization),
method: :delete,
class: "btn btn-danger",
data: {
turbo_frame: "_top",
turbo_confirm: false
} %>
</div>
</div>
<% end %>
Multi-Step Modal Workflows
๐ Complex Modal Interactions
Implement multi-step processes within modal contexts.
# Controller for multi-step modal
class Organizations::SetupController < ApplicationController
def step_1
respond_to do |format|
format.turbo_stream do
render turbo_stream: turbo_stream.replace(:modal,
partial: "shared/turbo_modal",
locals: {
modal_title: "Organization Setup - Step 1 of 3",
modal_size: "lg"
}
) do
render partial: "organizations/setup/step_1"
end
end
end
end
def step_2
if valid_step_1?
render turbo_stream: turbo_stream.replace(:inside_modal,
partial: "organizations/setup/step_2")
else
render turbo_stream: turbo_stream.replace(:inside_modal,
partial: "organizations/setup/step_1"),
status: :unprocessable_entity
end
end
def complete
if @organization.update(setup_params)
redirect_to organization_path(@organization),
notice: "Organization setup completed!"
else
render turbo_stream: turbo_stream.replace(:inside_modal,
partial: "organizations/setup/step_3"),
status: :unprocessable_entity
end
end
end
Summary
The Turbo Modal System provides:
๐ Complete Modal Solution
A comprehensive, accessible, and performant modal system that enhances user experience while maintaining modern development standards.
- ๐ Modern Architecture - Hotwire Turbo with Stimulus.js integration
- โฟ Accessibility First - WCAG 2.2 AA compliant implementation
- ๐จ Beautiful Design - Tailwind CSS 4.0 with dark mode support
- ๐ฑ Responsive Experience - Mobile-optimized across all devices
- โก High Performance - Efficient server-side rendering
- ๐ก๏ธ Robust Error Handling - Comprehensive form and validation support
- ๐ง Highly Customizable - Flexible sizing and styling options
๐ Modal Mastery Achieved!
You now have a complete understanding of implementing professional, accessible modals that enhance user experience while maintaining modern development standards. The system handles complex workflows while remaining simple to implement and maintain.