How to create Modals with Rails and Hotwire (and Tailwind CSS)

An abstract representation of modal in a duotone colorscheme

With the introduction of Hotwire in September of 2021, Rails developers could now create more advanced, JavaScript-based UI’s without writing hardly any JavaScript.

One of the most common UI elements in modern web-apps is the modal. Creating it with Hotwire is indeed quite straightforward, but there are few gotchas and some things to keep in mind. There have been a few articles and howto’s been written about it already, but I’d like to explore a few more (novel) ideas here.

In essence all a modal, sometimes also called a “dialog” is, is a contained component laid on top of the rest of the app (as a side-note: there’s a dialog element that can take care of most of the tricky bits too).

So this means with Rails and Hotwire we need two things:

  1. a turbo-frame to load the modals
  2. a wrapper around the actual modal content

The basics

Let’s add the turbo-frame to your application’s layout.

# app/views/layouts/application.htm.erb
<%= turbo_frame_tag "modal" %>

Then wrap the view you want as a modal like this:

# app/views/users/new.html.erb

<turbo-frame id="modal">
  # Modal wrapper
  <div role="dialog" tabindex="-1" class="fixed inset-0 z-30 flex items-center justify-center w-full h-screen p-2">

    # Backdrop
    <div class="fixed inset-0 block w-full h-screen cursor-default bg-gray-900/30"></div>

    <div class="relative z-20 w-full max-w-2xl max-h-screen overflow-y-auto bg-white shadow-lg rounded-md">
      # Modal's content, eg. a `form_with` helper

Then a way to view the modal:

# app/views/users/index.html.erb

<%= link_to "Create new user", new_user_path, data: {turbo_frame: "modal"} %>

Now when you click the button above, the content from app/views/ between the turbo-frame tag gets rendered in the turbo_frame_tag "modal" added to the app/views/layouts.html.erb. And because of the Tailwind CSS classes added, the content will “lay on top” of the app.

And that are the basics of getting a modal working in Rails app with Hotwire.

✨ Tip: for a quick way to get more advanced modal’s (including Stimulus), check out the modal from Rails Designer, built with ViewComponent, designed with Tailwind CSS and enhanced with Hotwire.

Beyond the basics

Well, those basics were easy enough, but if you opened the modal, you can’t close it now… That’s bad. We can improve this in few simple ways.

Let’s change the backdrop-div to be a button_to, like so:

<%= button_to nil, nil, type: :button, method: :get, data: {turbo_frame: "modal"}, class: "fixed inset-0 block w-full h-screen cursor-default bg-gray-900/30" %>

While this works, it’s not good practice (you can expect the developer console for the reason). Let’s up this modal with a simple Stimulus controller.

Enhance the modal with Stimulus

// app/javascript/controllers/modal_controller.js
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  connect() {

  hide() {
    this.#modalTurboFrame.src = null;

  hideOnSubmit(event) {
    if (event.detail.success) {

  disconnect() {
    this.#modalTurboFrame.src = null;

  // private

  get #modalTurboFrame() {
    return document.querySelector("turbo-frame[id='modal']");

Then update your app/views/users/new.html.erb like so:

<turbo-frame id="modal">
  < %= tag.button type: :button, data: {action: "modal#hide"}, class: "fixed inset-0 block w-full h-screen cursor-default bg-gray-900/30" %>

  <div data-controller="modal" data-action="turbo:submit-end->modal#hideOnSubmit keydown.esc->modal#hide" role="dialog" tabindex="-1" class="fixed inset-0 z-30 flex items-center justify-center w-full h-screen p-2">
    # …

All this Stimulus controller really does is reset the turbo-frame’s src attribute, when…

  • …clicking on the backdrop;
  • …a form is successfully submitted;
  • …pressing the Escape key.

And focus the element on connect(); possible because of the tabindex-attribute on the element.

If you want you can create a button inside the modal’s content to cancel (something you often see in modals, next to confirmation/save button), like so:

 <%= button_tag "Cancel", type: :button, method: :get, data: {action: "modal#hide"}, class: "px-3 py-1 text-sm leading-6 font-medium text-gray-700 bg-white border border-gray-200 rounded-md hover:border-gray-300" %>

Notice how it’s just a regular Rails button_tag, with a data-action attribute with the value of modal#hide. That’s the beauty of Stimulus, small, reusable JavaScript for the HTML you have.

Force a view in a modal

Another thing I’d like to add is force views to be viewed only as a modal (and as a standalone page, like “users/new.html.erb”). This is a tip taken from a previous article about ViewComponents.

It works with a small Rails controller concern:

# app/controllers/concerns/frameable.rb

module Frameable
  extend ActiveSupport::Concern


  def ensure_turbo_frame_response
    redirect_to root_path unless turbo_frame_request?

  def production_environment?

And now over at the UsersController, add this:

# app/controllers/users_controller.rb

class UsersController < ApplicationController
  before_action :ensure_turbo_frame_response, only: %w[new], if: :production_environment?

This will ensure users can only view the users/new view through a turbo-frame, ie. as a modal. But only when in production (if: :production_environment?). I like to add this flag because designing the view is quicker standalone than within the modal.

And that’s all there’s to it to get basic modals working in your Rails app. Feel free to send your suggestions or ideas over.

Published at . Have suggestions or improvements on this content? Do reach out. Interested in sharing Rails Designer with the Ruby on Rails community? Become an affiliate.

UI components for Ruby on Rails apps

$ 99 69 one-time

Get Access
  • One-time payment

  • Access to the entire library

  • Built for Ruby on Rails

  • Designed with Tailwind CSS and enhanced with Hotwire

  • Includes free updates (to any 1.x version)

  • Last update recently