Building a Svelte modal dialog component using the dialog element

Friday, October 27, 2023

sveltejavascriptdialog

First let’s make a barebones Svelte component which renders a dialog element with a slot. We’ll export a dialog property which refers to that dialog element.

Dialog.svelte
<script lang="ts">
  export let dialog: HTMLDialogElement;
</script>

<dialog bind:this={dialog}>
  <slot />
</dialog>

Then in our page we can import that component and use it to render a dialog with some content. We’ll bind that dialog property to a variable in our page so we can open and close the dialog.

page.svelte
<script lang="ts">
  import Dialog from './Dialog.svelte';

  let dialog: HTMLDialogElement;
</script>

<Dialog bind:dialog>
  <h1>Hi there!</h1>
  <button on:click={() => dialog.close()}>Close</button>
</Dialog>

<button on:click={() => dialog.showModal()}>Open the modal</button>

Hi there!

Try it out!

What you get for free

The native dialog element does a lot for us out of the box (or in it, I suppose).

  • The dialog has some basic styling: a white background, a black border, and a backdrop which darkens the rest of the page.
  • The dialog is automatically positioned in the center of the viewport.
  • If the content of the dialog is longer than the viewport, it will scroll internally, with a slight inset from the top and bottom of the viewport.
  • It traps focus while it is open, allowing you to tab through elements in the dialog. You can tab out of the dialog to focus other elements of the browser window, but other content in your page will not be focusable until the dialog is closed.
  • Pressing the escape key automatically closes the dialog.
  • You can even open a dialog from within another dialog, and the browser will keep track of which dialog is currently open. Pressing escape will only close one dialog at a time.
  • Probably some other built-in niceties that I haven’t noticed yet.

Hi there!

Cillum nostrud sint esse. Sint esse occaecat mollit incididunt. Occaecat mollit incididunt deserunt lorem eiusmod excepteur. Incididunt deserunt lorem eiusmod excepteur mollit. Lorem eiusmod excepteur mollit. Excepteur mollit reprehenderit excepteur ullamco proident in voluptate.

Elit ullamco irure adipiscing, do velit. Adipiscing do velit qui elit minim elit minim. Velit qui, elit minim elit minim. Minim, elit minim incididunt et adipiscing. Incididunt, et adipiscing aliqua. Aliqua, incididunt tempor id deserunt. Id deserunt proident ea eu incididunt mollit quis. Proident ea, eu incididunt mollit.

Enim proident ad lorem. Ad lorem ullamco anim, ea et. Anim, ea et lorem anim. Lorem, anim anim adipiscing aute. Adipiscing aute est ex mollit cupidatat.

Occaecat excepteur laborum sint veniam veniam. Laborum sint veniam veniam duis non eu do. Veniam veniam duis non eu, do. Non eu do proident cupidatat, do qui. Proident cupidatat do qui esse eiusmod ut. Do qui esse eiusmod, ut. Eiusmod ut cupidatat voluptate, labore fugiat voluptate occaecat. Voluptate labore fugiat, voluptate occaecat eu ad.

Veniam culpa aute qui commodo magna ut. Aute qui commodo, magna ut. Magna ut lorem et nisi tempor ex ipsum. Lorem, et nisi tempor. Tempor ex ipsum dolor. Ipsum dolor consectetur est nulla magna aute ut. Consectetur est nulla magna aute. Nulla magna aute ut sed. Aute ut, sed lorem nostrud. Lorem nostrud ex sunt do.

Consequat irure, commodo minim. Minim, ipsum tempor non. Non in, tempor elit. Elit, id amet dolore. Dolore officia veniam cupidatat id irure, ad ex. Cupidatat, id irure ad ex incididunt ullamco. Ad ex incididunt ullamco consequat, sed. Ullamco consequat, sed commodo.

Dolor nostrud nisi quis, id. Quis id cupidatat qui, consequat ipsum. Qui consequat ipsum ea eiusmod consequat cupidatat. Ipsum ea, eiusmod consequat cupidatat. Consequat cupidatat incididunt sint duis minim non ad. Incididunt sint duis minim non ad, exercitation exercitation. Minim non, ad exercitation. Exercitation exercitation aute sunt, quis sunt.

Quis cupidatat aliquip non nostrud laboris. Aliquip non, nostrud laboris commodo velit aliqua duis. Laboris commodo velit aliqua duis consequat dolor. Velit aliqua duis consequat dolor est. Duis consequat, dolor est. Est id in, culpa nulla eu esse sit.

Amet sed ipsum ut officia, fugiat cillum eu. Ut officia fugiat cillum eu reprehenderit excepteur sit. Fugiat cillum eu reprehenderit excepteur, sit ea. Reprehenderit excepteur sit ea in tempor exercitation sed. Sit, ea in tempor exercitation sed id. Tempor, exercitation sed id amet proident commodo minim. Id amet proident commodo, minim reprehenderit. Commodo minim reprehenderit sed adipiscing reprehenderit proident laborum. Reprehenderit sed adipiscing reprehenderit proident.

Elit ullamco irure adipiscing, do velit. Adipiscing do velit qui elit minim elit minim. Velit qui, elit minim elit minim. Minim, elit minim incididunt et adipiscing. Incididunt, et adipiscing aliqua. Aliqua, incididunt tempor id deserunt. Id deserunt proident ea eu incididunt mollit quis. Proident ea, eu incididunt mollit.

Enim proident ad lorem. Ad lorem ullamco anim, ea et. Anim, ea et lorem anim. Lorem, anim anim adipiscing aute. Adipiscing aute est ex mollit cupidatat.

Occaecat excepteur laborum sint veniam veniam. Laborum sint veniam veniam duis non eu do. Veniam veniam duis non eu, do. Non eu do proident cupidatat, do qui. Proident cupidatat do qui esse eiusmod ut. Do qui esse eiusmod, ut. Eiusmod ut cupidatat voluptate, labore fugiat voluptate occaecat. Voluptate labore fugiat, voluptate occaecat eu ad.

Veniam culpa aute qui commodo magna ut. Aute qui commodo, magna ut. Magna ut lorem et nisi tempor ex ipsum. Lorem, et nisi tempor. Tempor ex ipsum dolor. Ipsum dolor consectetur est nulla magna aute ut. Consectetur est nulla magna aute. Nulla magna aute ut sed. Aute ut, sed lorem nostrud. Lorem nostrud ex sunt do.

Consequat irure, commodo minim. Minim, ipsum tempor non. Non in, tempor elit. Elit, id amet dolore. Dolore officia veniam cupidatat id irure, ad ex. Cupidatat, id irure ad ex incididunt ullamco. Ad ex incididunt ullamco consequat, sed. Ullamco consequat, sed commodo.

Dolor nostrud nisi quis, id. Quis id cupidatat qui, consequat ipsum. Qui consequat ipsum ea eiusmod consequat cupidatat. Ipsum ea, eiusmod consequat cupidatat. Consequat cupidatat incididunt sint duis minim non ad. Incididunt sint duis minim non ad, exercitation exercitation. Minim non, ad exercitation. Exercitation exercitation aute sunt, quis sunt.

Quis cupidatat aliquip non nostrud laboris. Aliquip non, nostrud laboris commodo velit aliqua duis. Laboris commodo velit aliqua duis consequat dolor. Velit aliqua duis consequat dolor est. Duis consequat, dolor est. Est id in, culpa nulla eu esse sit.

Amet sed ipsum ut officia, fugiat cillum eu. Ut officia fugiat cillum eu reprehenderit excepteur sit. Fugiat cillum eu reprehenderit excepteur, sit ea. Reprehenderit excepteur sit ea in tempor exercitation sed. Sit, ea in tempor exercitation sed id. Tempor, exercitation sed id amet proident commodo minim. Id amet proident commodo, minim reprehenderit. Commodo minim reprehenderit sed adipiscing reprehenderit proident laborum. Reprehenderit sed adipiscing reprehenderit proident.

Another modal

Cillum nostrud sint esse. Sint esse occaecat mollit incididunt. Occaecat mollit incididunt deserunt lorem eiusmod excepteur. Incididunt deserunt lorem eiusmod excepteur mollit. Lorem eiusmod excepteur mollit. Excepteur mollit reprehenderit excepteur ullamco proident in voluptate.

Elit ullamco irure adipiscing, do velit. Adipiscing do velit qui elit minim elit minim. Velit qui, elit minim elit minim. Minim, elit minim incididunt et adipiscing. Incididunt, et adipiscing aliqua. Aliqua, incididunt tempor id deserunt. Id deserunt proident ea eu incididunt mollit quis. Proident ea, eu incididunt mollit.

Taking it up a notch

We get so much functionality with the dialog element that we almost don’t need to make a separate component. But having done so, we can now add some custom styling and other features to our Dialog component.

Some ideas to try:

  • Style the border, background, and backdrop of the dialog
  • Set a reasonable max-width
  • Add an optional title bar with a close button
  • Animate the modal opening and closing
  • Prevent scrolling the page content while the modal is open

Here’s a version of the Dialog component with some of these features added.

StyledDialog.svelte
<script lang="ts">
  import { onMount } from 'svelte';

  export let title: string | undefined = undefined;
  export let element: HTMLDialogElement;

  // Export a custom open function that calls the dialog element's showModal method.
  export function open() {
    element.showModal();
  }

  // Export a custom close function that sets a closing attribute on the dialog
  // element. This attribute is used to trigger the closing animation. So we add
  // a listener for animationend which calls the afterClosing method below. By
  // passing once: true, we don't have to remove this event listener.
  export function close() {
    element.addEventListener('animationend', afterClosing, { once: true });
    element.setAttribute('closing', '');
  }

  // When the closing animation completes, we can remove the closing attribute
  // and call the dialog's native close method.
  function afterClosing() {
    element.removeAttribute('closing');
    element.close();
  }

  // When the user presses the escape key, the browser calls the dialog's close
  // method directly, bypassing our nice closing animation. So we can listen for
  // the 'cancel' event, prevent its default behavior, and call our custom close
  // method instead.
  function onCancel(event: Event) {
    event.preventDefault();
    close();
  }

  // When the component mounts, add our event listener, and remove it on dismount.
  onMount(() => {
    element.addEventListener('cancel', onCancel);

    return () => {
      element.removeEventListener('cancel', onCancel);
    };
  });
</script>

<dialog bind:this={element}>
  {#if title}
    <div class="modal-title">
      <h2>{title}</h2>
      <button class="close-button" on:click={close}>&#x2717;</button>
    </div>
  {/if}
  <div class="modal-content">
    <slot />
  </div>
</dialog>

<style lang="less">
  // just using less for nesting syntax

  dialog {
    overscroll-behavior: contain;
    border: 1px solid rgba(0 0 0 / 0.3);
    border-radius: 0.5rem;
    box-shadow: 0 4px 10px rgba(0 0 0 / 0.3);
    max-width: min(95vw, 600px);
    padding: 0;

    // animate when the dialog opens
    &:is([open]) {
      animation: fade-in 0.2s ease-out, slide-in 0.2s ease-out;
      &::backdrop {
        animation: fade-in 0.2s ease-out;
      }
    }
    // animate when the dialog is closing
    &:is([closing]) {
      animation: fade-out 0.2s ease-out, slide-out 0.2s ease-out;
      &::backdrop {
        animation: fade-out 0.2s ease-out;
      }
    }

    // add a gradient and blur filter to the backdrop pseudo-element
    &::backdrop {
      background-image: linear-gradient(45deg, hsla(0 50% 50% / 0.5), hsla(200 50% 50%/ 0.5));
      backdrop-filter: blur(4px);
    }

    .modal-title {
      display: flex;
      flex-direction: row;
      justify-content: space-between;
      align-items: center;
      padding: 0.75rem 1rem;
      border-bottom: 1px solid rgba(0 0 0 / 0.1);
      font-size: 24px;
      position: sticky;
      top: 0;
      background-color: white;

      h2 {
        margin: 0;
      }

      .close-button {
        appearance: none;
        border: none;
        background: none;
        border: none;
        box-shadow: none;
        font-size: 1.25rem;
        border-radius: 4px;
        padding: 0;
        width: 2em;
        text-align: center;
        aspect-ratio: 1;
      }
    }

    .modal-content {
      padding: 1rem;
    }
  }

  @keyframes fade-in {
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  }

  @keyframes fade-out {
    from {
      opacity: 1;
    }
    to {
      opacity: 0;
    }
  }

  @keyframes slide-in {
    from {
      transform: translateY(-100%);
    }
    to {
      transform: translateY(0);
    }
  }

  @keyframes slide-out {
    from {
      transform: translateY(0);
    }
    to {
      transform: translateY(-100%);
    }
  }
</style>

Using the styled dialog component in a page

page.svelte
<script lang="ts">
  import StyledDialog from './StyledDialog.svelte';

  // Our local dialog variable is now an instance of the custom
  // component instead of an HTMLDialogElement
  let dialog: StyledDialog;
</script>

<!-- Using bind:this to bind the component instance to our local dialog variable -->
<StyledDialog bind:this={dialog} title="Hi there!">
  <p>Nulla amet anim laboris enim aute. Anim laboris...</p>
</StyledDialog>

<!-- Now we can call the exported open method on the dialog instance -->
<button on:click={() => dialog.open()}>Open the dialog</button>

Try it out

Much of the information and ideas in this post came from these two YouTube videos by CSS guru Kevin Powell:

And be sure to check out the MDN docs for HTMLDialogElement.

Addendum: this post was updated on November 18th, 2023 to use bind:this so that we can access the exported open/close methods on the component instance.

pascal’s diary · copyright about now · rss