--- name: dialog-patterns description: Native HTML dialog patterns for Rails with Turbo and Stimulus. Use when building modals, confirmations, alerts, or any overlay UI. Triggers on modal, dialog, popup, confirmation, alert, or toast patterns. --- # Native Dialog Patterns for Rails Build accessible, modern dialog UIs using the native HTML `` element with Turbo Frames and Stimulus. No JavaScript frameworks or heavy libraries required. ## When to Use This Skill - Building modal dialogs for forms, confirmations, or content - Creating toast/alert notifications - Implementing confirmation dialogs (delete, destructive actions) - Any overlay UI that needs focus management and accessibility ## Why Native ``? | Feature | Native `` | Custom Modal | |---------|-------------------|--------------| | Focus trapping | Built-in | Manual implementation | | ESC to close | Built-in | Manual implementation | | Backdrop | Built-in (`::backdrop`) | Manual overlay | | Accessibility | Native `role="dialog"` | Manual ARIA | | Top layer | Automatic (above all content) | z-index battles | | Scroll lock | Automatic | Manual `overflow: hidden` | ## Zero-JavaScript Confirmation Dialogs (Recommended) Modern browsers support the **Invoker Commands API** for declarative dialog control—no JavaScript required. See [references/zero-js-patterns.md](references/zero-js-patterns.md) for complete examples. ### Quick Reference ```erb <%= button_tag "Delete", commandfor: "delete-#{post.id}", command: "show-modal" %>

Delete "<%= post.title %>"?

<%= button_to "Delete", post, method: :delete %>
``` ### Key Attributes | Attribute | Purpose | |-----------|---------| | `commandfor="id"` | References the dialog to control | | `command="show-modal"` | Opens as modal (backdrop, focus trap) | | `command="close"` | Closes the dialog | | `closedby="any"` | Enables backdrop click and ESC to close | ### When to Use Zero-JS vs Stimulus | Scenario | Approach | |----------|----------| | Simple confirmations | Zero-JS (Invoker Commands) | | Modals with async content | Stimulus + Turbo Frames | | Complex multi-step dialogs | Stimulus controller | | Animations | CSS `@starting-style` | ### Additional Patterns (see references/) - **CSS animations** with `@starting-style` for enter/exit transitions - **Turbo.config.forms.confirm** to replace ugly browser dialogs - **Progressive enhancement** for cross-browser compatibility ## Core Pattern: Async Modal with Turbo Frames The recommended pattern for Rails modals combines three technologies: 1. **Turbo Frame** - Async content loading without page reload 2. **Native ``** - Accessible modal presentation 3. **Stimulus controller** - Lifecycle management ### Step 1: Layout Container Add a modal turbo-frame to your layout: ```erb <%# app/views/layouts/application.html.erb %> <%= yield %> <%# Modal injection point %> <%= turbo_frame_tag :modal %> ``` ### Step 2: Trigger Links Target the modal frame from any link: ```erb <%# Any view %> <%= link_to "New Post", new_post_path, data: { turbo_frame: :modal } %> <%= link_to "Edit", edit_post_path(@post), data: { turbo_frame: :modal } %> <%= link_to "Confirm Delete", confirm_delete_post_path(@post), data: { turbo_frame: :modal } %> ``` ### Step 3: Modal Content View Wrap modal content in matching turbo-frame with nested inner frame: ```erb <%# app/views/posts/new.html.erb %> <%= turbo_frame_tag :modal do %> <%# Inner frame prevents flash during form validation %> <%= turbo_frame_tag :modal_content do %>

New Post

<%= render "form", post: @post %>
<% end %> <% end %> ``` ### Step 4: Stimulus Controller Key behaviors: `showModal()` on connect, `replaceChildren()` on disconnect (prevents stale content), `clickOutside` for backdrop close. See [references/dialog-examples.md](references/dialog-examples.md) for full Stimulus controller, CSS styling, and Tailwind variant. ## Why Nested Turbo Frames? The nested frame pattern (`modal` > `modal_content`) prevents content flashing: ```erb <%= turbo_frame_tag :modal do %> <%= turbo_frame_tag :modal_content do %> ... <% end %> <% end %> ``` **Problem without nested frame:** When a form inside the modal has validation errors and re-renders, the outer frame briefly shows the old content before replacing it. **Solution with nested frame:** The inner frame handles form re-renders independently, keeping the modal structure stable. ## Form Handling in Modals ### Successful Submission Redirect with Turbo to close modal and update page: ```ruby # app/controllers/posts_controller.rb def create @post = Post.new(post_params) if @post.save redirect_to posts_path, notice: "Post created!" else render :new, status: :unprocessable_entity end end ``` The redirect navigates `_top` (full page), effectively closing the modal. ### Validation Errors Re-render the form with `422` status to keep modal open: ```ruby render :new, status: :unprocessable_entity ``` ### Turbo Stream Response (Stay in Modal) Use `turbo_stream.update("modal", "")` to clear modal without full redirect. See [references/dialog-examples.md](references/dialog-examples.md) for full example. ## Confirmation Dialog Pattern For destructive actions: add a `confirm_delete` member route, render a dialog in a turbo frame, trigger via `link_to` with `data: { turbo_frame: :modal }`. See [references/dialog-examples.md](references/dialog-examples.md) for full confirmation dialog view, route, and trigger. ## Alert/Toast Pattern For flash messages and notifications. Use `show()` instead of `showModal()` for non-modal presentation. See [references/toast-slideover-patterns.md](references/toast-slideover-patterns.md) for complete implementation. ```erb

<%= message %>

``` Key difference: `show()` opens without backdrop or focus trap (toasts), `showModal()` centers with backdrop (modals). ## Slideover Panel Pattern For side panels (settings, filters, details). See [references/toast-slideover-patterns.md](references/toast-slideover-patterns.md) for styling and animations. ```erb ``` ## Accessibility Native `` provides focus trapping, ESC close, background inert, and top layer automatically. Additionally ensure: - Visible close button (not just ESC) - `aria-labelledby` / `aria-describedby` for descriptive context - Focus return to trigger element on close (store `document.activeElement` in `connect()`) See [references/dialog-examples.md](references/dialog-examples.md) for enhanced accessibility and focus return examples. ## Common Patterns Summary | Pattern | Container | Stimulus | `show` method | |---------|-----------|----------|---------------| | Modal form | `turbo_frame_tag :modal` | `dialog` | `showModal()` | | Confirmation | `turbo_frame_tag :modal` | `dialog` | `showModal()` | | Toast/Alert | Fixed position | `toast` | `show()` | | Slideover | `turbo_frame_tag :modal` | `dialog` | `showModal()` | ## Anti-Patterns to Avoid | Anti-Pattern | Problem | Solution | |--------------|---------|----------| | Custom modal without `` | No native accessibility | Use native `` | | Missing nested turbo-frame | Content flash on validation | Add inner frame | | Not clearing frame on close | Stale content on reopen | Clear with `replaceChildren()` in `disconnect()` | | z-index for stacking | Battles with other elements | `` uses top layer | | Manual focus trap | Complex, error-prone | `showModal()` handles it | | Inline backdrop div | Extra markup | Use `::backdrop` pseudo-element | ## Testing Dialogs ```ruby # System test - use `within "dialog"` to scope assertions within "dialog" do fill_in "Title", with: "My Post" click_button "Create" end expect(page).not_to have_selector("dialog[open]") # Modal closed ``` ## Browser Support | Pattern | Chrome | Firefox | Safari | |---------|--------|---------|--------| | Native `` | 37+ | 98+ | 15.4+ | | Invoker Commands | 135+ | 144+ | 26.2+ | | `@starting-style` | 117+ | 129+ | 17.5+ | For older browsers: [dialog polyfill](https://github.com/GoogleChrome/dialog-polyfill), [invokers polyfill](https://github.com/nickshanks/invokers). See [references/zero-js-patterns.md](references/zero-js-patterns.md) for progressive enhancement strategies.