DEV Community

thomasvanholder
thomasvanholder

Posted on • Edited on

Create a modal with the HTML dialog element, Tailwind and Stimulus

Support for the dialog element is now widespread, with safari being the last of the main browsers to provide native support.

The dialog HTML element is an appealing choice for creating a modal as it comes with built-in functionality for opening and closing modals.

This tutorial uses Stimulus as a light-weight JavaScript framework. To learn more about Stimulus, visit the docs. When you use vanilla JS, you can mimic the data-action and data-target behavior in by utilizing the appropriate document.querySelectors and eventListeners instead.

The form and button styling are extrapolated to improve readability. See here for a full gist of the CSS, HTML and JS.



1. Add button and dialog

To start, let's create a button and dialog element wrapped in a stimulus controller.



<div data-controller="modal">
  <button data-action="modal#open" class="btn-outline">Open modal</button>

  <dialog data-modal-target="modal">
    <button type="button" data-action="modal#close" class="absolute top-4 right-4">X</button>

    <h1>Reset your password</h1>

    <hr class="mb-4">

    <form class="space-y-4">
      <label>Email</label>
      <input type="text">

      <button type="button" data-action="modal#close" class="btn-outline">Close</button>
      <button type="button" class="btn-blue">Reset Password</button>
    </form>
  </dialog>
</div>


Enter fullscreen mode Exit fullscreen mode

The corresponding stimulus controller is pretty barebones.



//app/javascript/controllers/modal_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["modal"]

  open(event) {
    event.preventDefault();

    this.modalTarget.showModal();
  }

  close(event) {
    event.preventDefault();

    this.modalTarget.close();
  }
}


Enter fullscreen mode Exit fullscreen mode
  • The showModal() method displays the modal on the top layer and adds a default background. Any interaction outside the dialog is inaccessible.
  • The close() method simply closes the dialog.

Button and dialog


2. Close modal by esc key

This is an easy one. Closing the modal by escape key is included by default when utilising the dialog element. Nice!


3. Close modal by backdrop click

  • Remove all default padding on the dialog element to ensure only a background click triggers the closing of the modal.
  • Add padding on the h1 and form element instead.


<dialog data-modal-target="modal" class="p-0">
 ...
 <h1 class="p-4">Reset your password</h1>

 <form class="p-4 space-y-4">
   ...
 </form>
</dialog>


Enter fullscreen mode Exit fullscreen mode
  • Add an eventListener that initiates after opening the modal to listen for the backdrop click.


export default class extends Controller {
  // ....
  open(event) {
    event.preventDefault();

    this.modalTarget.showModal();

    this.modalTarget.addEventListener('click', (e) =>  this.backdropClick(e));
  }

  backdropClick(event) {
    event.target === this.modalTarget && this.close(event)
  }
}


Enter fullscreen mode Exit fullscreen mode
  • The backdropClick() method verifies if the clicked element is the dialog and, if so, closes the modal.

4. Reset form after modal close

reset form

The form still populates previously filled input after closing and re-opening the modal. To start the modal with empty inputs, add a form target and reset the form.



<form class="p-4 space-y-4" data-modal-target="form">
  ...
</form>


Enter fullscreen mode Exit fullscreen mode


import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["modal", "form"]

  close(event) {
    event.preventDefault();

    this.modalTarget.close();

    this.formTarget.reset() // or find by id selector
  }
}


Enter fullscreen mode Exit fullscreen mode

5. Change default backdrop style

Add the backdrop selector to the dialog html element.



<dialog class="p-0 backdrop:bg-gray-400 backdrop:bg-opacity-50">
 ...
</dialog>


Enter fullscreen mode Exit fullscreen mode

6. Add animation

The default animation is quite abrupt and appears instantaneous on the screen. To make the transition smoother, let's add a fade-in custom animation with a duration of 0.5 seconds.

In the tailwind.config.js file, add a fade-in keyframe and animation. The effect can be applied using the animate-fade-in class.



module.exports = {
  // ... 
  theme: {
    extend: {
      keyframes: {
        "fade-in": {
          '0%': { opacity: '0%' },
          '100%': { opacity: '100%' },
        }
      },
      animation: {
        "fade-in": 'fade-in 0.5s ease-in-out',
      } 
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

Next, add the newly created animation to the dialog element using the open modifier: open:animate-fade-in.



<dialog data-modal-target="modal" class="p-0 backdrop:bg-gray-400 backdrop:bg-opacity-50 open:animate-fade-in">
 ...
</dialog>


Enter fullscreen mode Exit fullscreen mode

You can easily add this animation to the backdrop as well: open:backdrop:animate-fade-in.



<dialog data-modal-target="modal" class="p-0 backdrop:bg-gray-400 backdrop:bg-opacity-50 open:animate-fade-in open:backdrop:animate-fade-in">
 ...
</dialog>


Enter fullscreen mode Exit fullscreen mode

That's it! Using the dialog element simplifies creating modals by leveraging its build-in API features.

Final

Top comments (4)

Collapse
 
superails profile image
Yaroslav Shmarov • Edited

instead of

<button type="button" data-action="modal#close" class="btn-outline">Close</button>
Enter fullscreen mode Exit fullscreen mode

you can use

<button formmethod="dialog" type="submit">Close</button>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
superails profile image
Yaroslav Shmarov

as you mentioned on the beginning of the article, isn't the whole point of stimulus to not use eventlistener?

just add <dialog data-action="click->modal#clickOutside"> and

  clickOutside(event) {
    // kinda inverted
    if (event.target === this.dialogTarget) {
      this.close()
    }
  }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ianengelbrecht profile image
Ian Engelbrecht

How do you prevent the content behind the dialog from scrolling when opening and closing the dialog. I have to scroll down on my page to get to the button that opens the dialog, but when I open the dialog the page jumps back to the top.

Collapse
 
superails profile image
Yaroslav Shmarov

something like this:

open() {
    document.body.classList.add("overflow-hidden");
}

close() {
    document.body.classList.add("overflow-hidden");
}
Enter fullscreen mode Exit fullscreen mode