DEV Community

Rails Designer
Rails Designer

Posted on • Originally published at railsdesigner.com

Use native dialog with Turbo (and no extra JavaScript)

This article was originally published on the Rails Designer Blog


Building modals and sliders previously meant reaching for a Stimulus controller. But what if you could create them without writing any JavaScript at all? Like none, zip, nada, nothing. Something that looks like this:

This article shows how to use the native <dialog> element combined with Attractive.js (a JS-free JS library I published recently) to build modals and sliders that work together with Turbo Frames. No custom JavaScript required. 🤯

As always the code can be found on GitHub.

The basics

The foundation is a single <dialog> element in your layout. Start by adding it to your application.html.erb:

<dialog
  id="overlay"
  closedby="any"
  class="
    px-3 py-4 max-w-md w-full
    m-auto rounded-lg
    opacity-100 scale-100
    shadow-2xl
    starting:opacity-0 starting:scale-95
    transition-all duration-300
    backdrop:bg-black/50 backdrop:backdrop-blur-sm
  "
>
  <%= tag.turbo_frame id: :modal %>
</dialog>
Enter fullscreen mode Exit fullscreen mode

The closedby="any" attribute means clicking outside the dialog or pressing escape will close it. The Turbo Frame inside provides the content target. The CSS classes handle the fade and scale animation using the starting: pseudo-class. This is using Tailwind CSS, but you can of course use @starting-style.

Now add Attractive.js to your JavaScript. Import it in app/javascript/application.js (you can install however else you want; see the docs:

 // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
 import "@hotwired/turbo-rails"
 import "controllers"
+
+import attractivejs from "https://esm.sh/attractivejs";
Enter fullscreen mode Exit fullscreen mode

Attractive.js provides the dialog#openModal action that opens the dialog element. No custom controller needed.

You can then use it like this:

<%= link_to "Show modal", modal_path, data: {action: "dialog#openModal", target: "#overlay", turbo_frame: :modal} %>
Enter fullscreen mode Exit fullscreen mode

The data-action attribute tells Attractive.js to open the dialog. The data-target points to the dialog element. The data-turbo-frame tells Turbo to load the response into the modal frame. Simple!

Click the link and the modal opens. Click outside or press escape and it closes.

Supporting both modals and sliders

Now extend this to support both modals and sliders using the same dialog element. The trick is using a type attribute to differentiate between them.

First update the dialog styles to handle both types:

<dialog
  id="overlay"
  closedby="any"
  class="
    px-3 py-4 max-w-md w-full
    opacity-100 scale-100 translate-x-0 translate-y-0
    shadow-2xl
    starting:opacity-0
    transition-all duration-300

    /* Modal-specific */
    [&[type=modal]]:m-auto
    [&[type=modal]]:rounded-lg
    [&[type=modal]]:starting:scale-95
    [&[type=modal]]:backdrop:bg-black/50 [&[type=modal]]:backdrop:backdrop-blur-sm
    [&[type=modal]]:max-sm:mb-0 [&[type=modal]]:max-sm:rounded-b-none
    [&[type=modal]]:max-sm:starting:translate-y-full
    [&[type=modal]]:max-sm:starting:scale-100
    [&[type=modal]]:sm:my-auto

    /* Slider-specific */
    [&[type=slider]]:m-0 [&[type=slider]]:ml-auto
    [&[type=slider]]:h-screen [&[type=slider]]:max-h-none [&[type=slider]]:max-w-sm
    [&[type=slider]]:rounded-l-lg
    [&[type=slider]]:starting:translate-x-full
  "
>
  <%= tag.turbo_frame id: :modal %>
</dialog>
Enter fullscreen mode Exit fullscreen mode

The [&[type=modal]] and [&[type=slider]] selectors apply different styles based on the type attribute. Modals center themselves and scale in. Sliders stick to the right edge and slide in from the side.

On mobile, modals slide up from the bottom instead of scaling. Sliders maintain their slide-in behavior across all screen sizes. Just as seen in the Gif above.

Now update the links to set the type attribute before opening:

<%= link_to "Show modal", modal_path, data: {action: "addAttribute#type=modal dialog#openModal", target: "#overlay", turbo_frame: :modal} %>
Enter fullscreen mode Exit fullscreen mode

The addAttribute#type=modal action from Attractive.js sets the type attribute on the dialog before opening it. This triggers the modal-specific styles. The order is important as you want the type to be set first, otherwise styles from another type might leak through.

Add another modal endpoint to show it works with multiple modals:

def another_modal
  render html: helpers.tag.turbo_frame("Another modal content", id: :modal)
end
Enter fullscreen mode Exit fullscreen mode

And the corresponding link:

<%= link_to "Show another modal", another_modal_path, data: {action: "addAttribute#type=modal dialog#openModal", target: "#overlay", turbo_frame: :modal} %>
Enter fullscreen mode Exit fullscreen mode

Now add a slider endpoint:

def slider
  render html: helpers.tag.turbo_frame("Slider content", id: :modal)
end
Enter fullscreen mode Exit fullscreen mode

With its link:

<%= link_to "Show slider", slider_path, data: {action: "addAttribute#type=slider dialog#openModal", target: "#overlay", turbo_frame: :modal} %>
Enter fullscreen mode Exit fullscreen mode

The only difference is type=slider instead of type=modal. Same dialog element, same Turbo Frame, different presentation. The CSS handles everything else.


Browsers get more and more powerful features, like dialog. Using these keeps your code simple and easier to maintain. The native dialog element provides accessibility features like focus trapping and escape key handling. There is also the Invoke Commands API that means even some of the Attractive.js interactivity without would not be needed. Although setting the Turbo Frame src attribute would still be needed. ❤️

Top comments (0)