Inorganik Produce

A simple hash-routed dialog system with Svelte

October 8, 2023

For my project I wanted to be able to open certain dialogs from any component in my app, such as login and settings dialogs. I wanted to do this without context or emitting click handlers, and without inserting the same dialog components all over the place. With this solution, you can have a single instance of your dialogs and have them open based on the url hash. To open a dialog you can simply use an anchor:

<a href="#login">Login</a>

The second thing is, I wanted a dialog component without using any third-party UI libraries. And guess what? Native HTML gives us the <dialog> element which has some special abilities. So this solution is dependency-free! We'll look at <dialog> in a moment. First let's create a hash store.

Hash store

lib/state/hash.js:

import { writable } from 'svelte/store'

export function createHashStore() {
  if (typeof window === 'undefined') {
    const { subscribe } = writable('')
    return { subscribe }
  }

  const hash = writable(window.location.hash)

  function updateHash() {
    hash.set(window.location.hash)
  }
  window.removeEventListener('hashchange', updateHash, true)
  window.addEventListener('hashchange', updateHash, true)

  return hash
}

export default createHashStore()

This code creates a store that updates every time the hash of the url changes, and is based on a url store snippet by Svelte core team member Bjorn Lu. You can import this store in as many components as you like and it only ever creates one store. You use it like this:

MyComponent.svelte:

<script>
  import hash from '$lib/state/hash.js'

  $: dialogOpen = $hash === '#login'
</script>

Now dialogOpen automatically updates to true if the hash matches "#login". From here, you can bind dialogOpen to the dialog's open attribute, or use it to call the dialog's showModal() method.

The dialog element

<dialog> is something I stumbled across, as I have been using UI component libraries for so long that I didn't realize how far html and browser support has come. With the native dialog, you get an accessible, versatile dialog with no extra packages.

By just getting a reference to the dialog element, you have access to special methods, dialog.showModal() and dialog.close(). Check out the docs for more. Although it's very simple to just bind our dialogOpen property to the dialog's open attribute, I've found it's more accessible to use the showModal() method, because that way the close button is auto-focused and you can press escape to close the dialog.

MyComponent.svelte:

<script>
  import hash from '$lib/state/hash.js'

  let dialog

  $: if ($hash === '#login') {
    dialog?.showModal()
  } else {
    dialog?.close()
  }
</script>

<dialog bind:this={dialog}>
  ...

In closing (the dialog)

We can listen for the dialog's close event to update the hash and react to any input provided like so:

<script>
  ...

  function handleClose() {
    window.location.hash = '';
    // do stuff
  }
</script>

<dialog bind:this={dialog} on:close={handleClose}>
  ...

It's worth mentioning, if you have a form inside your dialog, there are some special things you can do. For instance if you set the form's method attribute to "dialog", submitting it will close the dialog. In addition, if you place a button like this in your form, it will close the dialog:

<button value="foo" formmethod="dialog">Close</button>

For either of these methods, dialog.returnValue will get set with the value of the button, in this case "foo".

Conclusion

Dialogs are shown and hidden with the display property, but there is a lot of opportunity for styling improvement here. Open dialogs have the open attribute which can be used for an open dialog selector in your CSS, e.g. dialog[open].

I personally really like how Daisy UI, a Tailwind add-on handles dialogs. You get a really nice fade-and-scale animation simply by adding the "modal" class, no javascript required.

I hope this was useful for your Svelte project. Check back later for more frontend development articles!

Links

My apps

Nomaste
Ghost AR