Non-tech founder’s guide to choosing the right software development partner Download Ebook
Home>Blog>Async modal on rails with native <dialog> element

Async Modal on Rails with Native <dialog> Element

In this post, I'll show you how to create asynchronous modal windows in Rails using Turbo Frames and the native <dialog> element. This approach combines the power of Rails' Hotwire stack with modern web standards to create smooth, accessible modal experiences without heavy JavaScript frameworks. We'll build a complete example using a login form that loads dynamically and displays in a native dialog with proper focus management and backdrop handling.

Step 1: Add Container to Layout

Add a turbo-frame container for modals in your main layout:


<body>

  <!-- ...existing code... -->

  <%= turbo_frame_tag :modal %>

</body>

Step 2: Configure Link to Open Modal

Create a link that will load content into our modal frame:


<%= link_to "Sign In",  login_path, data: { turbo_frame: :modal } %>

On click, the content will be loaded asynchronously and injected into the turbo_frame_tag :modal.

Step 3: Wrap Form in turbo_frame

In the login form view, wrap the content in the corresponding turbo-frame:


<%= turbo_frame_tag :modal do %>

  <dialog data-controller="modal" class="modal">

    <!-- We need one more turbo_frame_tag to prevent the modal from hiding and showing again when validation occurs or when navigating to another page inside the modal -->

    <%= turbo_frame_tag :modalContent do %>

      <div class="modal__header">

        <h3 class="modal__title">Sign In</h3>

        <button type="button" class="modal__close" data-action="click->modal#close">

          <%= vite_icon_tag "close.svg", class: "size-6" %>

          <span class="sr-only">Close Modal</span>

        </button>

      </div>



      <%= form_with url: rodauth.login_path, method: :post, class: "form" do |form| %>

        <div class="modal__body">

          <%= form.email_field rodauth.login_param, autofocus: true, required: true %>

          <%= form.password_field rodauth.password_param, required: true %>

        </div>

        <div class="modal__footer">

          <%= f.submit "Sign In" %>

        </div>

      <% end %>

    <% end %>

  </dialog>

<% end %>

Step 4: Create Stimulus Controller

Now create a controller that will manage the modal window:


import { Controller } from '@hotwired/stimulus'

import { stimulus } from '~/init'



export default class ModalController extends Controller {

  connect () {

    this.element.addEventListener('click', this.#closeOnBackdropClick.bind(this))

    this.element.showModal()

  }



  disconnect () {

    this.element.removeEventListener('click', this.#closeOnBackdropClick.bind(this))

    this.close()

  }



  show () {

    this.element.showModal()

  }



  close () {

    try {

      this.element.close()

      ModalController.turboFrame.src = null

      this.element.remove()

    } catch (e) {}

  }



  #closeOnBackdropClick (event) {

    if (event.target === this.element) {

      this.close()

    }

  }



  static get turboFrame () {

    return document.querySelector('turbo-frame[id=\'modal\']')

  }

}



stimulus.register('modal', ModalController)



How It Works

  1. On link click, Turbo sends an request and loads content into turbo_frame_tag :modal

  2. As soon as content is loaded, the Stimulus controller's connect() method fires, which: Opens the dialog via showModal() and adds event listener for backdrop click closing

  3. When closing the modal (close() method): Closes the dialog. Important! Clears the turbo-frame src and removes the element to prevent flashing of old content when opening the next modal

  4. When controller disconnects (disconnect()): Removes all event listeners and closes the modal

Benefits of This Approach

  • ✅ Uses native <dialog> element

  • ✅ Asynchronous content loading

  • ✅ Automatic focus management and accessibility

  • ✅ Simple integration with Rails and Turbo

  • ✅ Doesn't pollute DOM when closed

Additional Notes

For more complex cases, I recommend using ViewComponent - it allows you to create reusable modal components with clear structure and better code organization.

The key insight here is that the modal opens automatically when content loads (via connect()) and cleans up after itself when closed (via clearing the turbo-frame), preventing any visual artifacts when opening different modals.

Additional Resources

Categories:

Discover the Products We’ve Developed

Let's Build Something Great Together

Let's discuss your project and explore how a Rails upgrade can become your competitive advantage. Contact us today to start the conversation.

*By submitting this form, you agree with JetRockets’ Privacy Policy