February 21, 2021

How to create modals using Hotwire

A month ago I shared a video on Twitter, showing a nice interaction using Turbo and a little bit of Stimulus.

In this article I'll show you how I've implemented modals in Spina CMS. You'll be amazed at how little code you need to make this work with Hotwire.

Step 1
Start by adding a <turbo-frame> to your layout and give it an ID (i.e. modal)
<body>
  ...
  <%= turbo_frame_tag "modal" %>
</body>
Step 2
Create a link where you want to trigger a modal window. In this example I want to show a modal when a user clicks the New page button. Add the data-turbo-frame attribute and set it to the ID you used in step 1.
<%= link_to spina.new_admin_page_path, data: {turbo_frame: "modal"} do %>
  New page
<% end %>
Step 3
The pages#new action must return a turbo-frame tag with your modal ID containing your modal HTML. I've omitted any CSS in this code snippet. Spina uses Tailwind CSS to create a fixed positioned overlay which includes a semi-transparent backdrop and the modal window. It's a great idea to use something like ViewComponent to render this view.

Here's the HTML of our base template:
<turbo-frame id="modal">
  <div class="modal" data-controller="modal" data-action="keyup@document->modal#escClose">
    <button type="button" class="modal-backdrop" tabindex="-1" data-action="modal#close"></button>

    <div class="modal-window">
      <!-- New page form -->
    </div>

  </div>
</turbo-frame>
Step 4
We haven't had to use any custom javascript up until this point and we already have a working modal window! To make it easier to close modals, I've added a modal-controller to manage closing the modal using either a button or the escape key. Remember to set the Turbo frame's src to nil after closing, otherwise you won't be able to open the same modal a second time.
import { Controller } from "stimulus"

export default class extends Controller {

  close() {
    this.element.remove()
    this.modalTurboFrame.src = null
  }
  
  escClose(event) {
    if (event.key === 'Escape') this.close()
  }

  get modalTurboFrame() {
    return document.querySelector("turbo-frame[id='modal']")
  }
}
Optional: Tailwind CSS
These are the classes I used to style modals in Spina CMS. It includes a backdrop filter to blur everything below the modal. 
.modal {
  @apply flex items-center justify-center;
  @apply fixed z-40 inset-0 h-full p-6;
}
  
.modal-window {
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  @apply w-full max-w-lg overflow-hidden relative;
  @apply bg-white bg-opacity-75 shadow-lg rounded-xl;
  @apply border border-gray-400;
}

.modal-backdrop {
  @apply cursor-default;
  @apply w-full h-full fixed inset-0;
  @apply bg-gray-700 bg-opacity-25;
}
Side note: I created these classes using Tailwind's @apply for brevity in my examples. Better to use the util classes directly in a ViewComponent.