DEV Community

Cover image for 🚀 Svelte Quick Tip: Creating a toast notification system
Dana Woodman
Dana Woodman

Posted on

🚀 Svelte Quick Tip: Creating a toast notification system

Toast you say? 🍞

A common UI design pattern is to use "toasts" or small UI notifications that alert the user of something happening in realtime (e.g. a form submission error, a new message or friend request, etc).

In this article, we will be building a simple toast system in Svelte, kinda like this:

toasts

Impatient? See the REPL here


Create a Svelte store for out toast notifications

Let's start out by creating a simple Svelte store for our toast system. The store will just contain an array that we will update when a new toast is created or "dismissed":

import { writable } from 'svelte/store'

export const toasts = writable([])

export const dismissToast = (id) => {
  toasts.update((all) => all.filter((t) => t.id !== id))
}

export const addToast = (toast) => {
  // Create a unique ID so we can easily find/remove it
  // if it is dismissible/has a timeout.
  const id = Math.floor(Math.random() * 10000)

  // Setup some sensible defaults for a toast.
  const defaults = {
    id,
    type: 'info',
    dismissible: true,
    timeout: 3000,
  }

  // Push the toast to the top of the list of toasts
  const t = { ...defaults, ...toast }
  toasts.update((all) => [t, ...all])

  // If toast is dismissible, dismiss it after "timeout" amount of time.
  if (t.timeout) setTimeout(() => dismissToast(id), t.timeout)
}
Enter fullscreen mode Exit fullscreen mode

Overall this should be pretty simple, we have a two methods, one for adding a toast and the other for removing. If the toast has a timeout field, we set a timeout to remove the toast. We set some default values for all toasts and we give a toast an id to make it easier to add/remove and for Svelte's {#each} tag to index it better.


Create the toasts parent component

<script lang="ts">
  import Toast from './Toast.svelte'

  import { dismissToast, toasts } from './store'
</script>

{#if $toasts}
  <section>
    {#each $toasts as toast (toast.id)}
      <Toast
        type={toast.type}
        dismissible={toast.dismissible}
        on:dismiss={() => dismissToast(toast.id)}>{toast.message}</Toast>
    {/each}
  </section>
{/if}

<style lang="postcss">
  section {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    width: 100%;
    display: flex;
    margin-top: 1rem;
    justify-content: center;
    flex-direction: column;
    z-index: 1000;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

Create the toast component

Next, we're going to create a Toast.svelte component with different states: success, error, and info, like so:

toasts

<script>
  import { createEventDispatcher } from 'svelte'
  import { fade } from 'svelte/transition'
  import SuccessIcon from './SuccessIcon.svelte'
  import ErrorIcon from './ErrorIcon.svelte'
  import InfoIcon from './InfoIcon.svelte'
  import CloseIcon from './CloseIcon.svelte'

  const dispatch = createEventDispatcher()

  export let type = 'error'
  export let dismissible = true
</script>

<article class={type} role="alert" transition:fade>
  {#if type === 'success'}
    <SuccessIcon width="1.1em" />
  {:else if type === 'error'}
    <ErrorIcon width="1.1em" />
  {:else}
    <InfoIcon width="1.1em" />
  {/if}

  <div class="text">
    <slot />
  </div>

  {#if dismissible}
    <button class="close" on:click={() => dispatch('dismiss')}>
      <CloseIcon width="0.8em" />
    </button>
  {/if}
</article>

<style lang="postcss">
  article {
    color: white;
    padding: 0.75rem 1.5rem;
    border-radius: 0.2rem;
    display: flex;
    align-items: center;
    margin: 0 auto 0.5rem auto;
    width: 20rem;
  }
  .error {
    background: IndianRed;
  }
  .success {
    background: MediumSeaGreen;
  }
  .info {
    background: SkyBlue;
  }
  .text {
    margin-left: 1rem;
  }
  button {
    color: white;
    background: transparent;
    border: 0 none;
    padding: 0;
    margin: 0 0 0 auto;
    line-height: 1;
    font-size: 1rem;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

Hopefully this component is pretty straight forward; it's just some styling for the toast, some conditions for if it is "dismissible" and come icon components (which are just SVGs).


Creating toast notifications

You can now create toast notifications anywhere in your Svelte app (in your JS files or your .svelte files):

import { addToast } from "./store";

addToast({
  message: "Hello, World!",
  type: "success",
  dismissible: true,
  timeout: 3000,
});
Enter fullscreen mode Exit fullscreen mode

You can then use your <Toasts /> component somewhere in your layout component (e.g. App.svelte or _layout.svelte, etc).


Wrapping up 🌯

That's it folks, hopefully you learning something today!

See the full toast system in the Svelte REPL here.

Thanks for reading!


Thanks for reading! Consider giving this post a ❤️, 🦄 or 🔖 to bookmark it for later. 💕

Have other tips, ideas, feedback or corrections? Let me know in the comments! 🙋‍♂️

Don't forget to follow me on Dev.to (danawoodman), Twitter (@danawoodman) and/or Github (danawoodman)!

Photo by Joshua Aragon on Unsplash

Top comments (5)

Collapse
 
geoffrich profile image
Geoff Rich

Wow you get these out fast 😅

It's good to see role="alert" on the toast for accessibility!

It would also be good to make sure the close button has an accessible name and focus styles for keyboard and assistive tech users. Otherwise the button will receive focus, but not indicate its purpose.

<button class="close" on:click={() => dispatch("dismiss")}>
    <!-- 
        This provides a name that will be read out by screen readers, 
        but not shown visually 
    -->
    <span class="visually-hidden">Close</span>
    <CloseIcon width="0.8em" />
</button>

<style>
    button:focus {
        outline: 1px solid black;
        outline-offset: 2px;
    }
    /* from https://piccalil.li/quick-tip/visually-hidden */
    .visually-hidden {
        border: 0;
        clip: rect(0 0 0 0);
        height: auto;
        margin: 0;
        overflow: hidden;
        padding: 0;
        position: absolute;
        width: 1px;
        white-space: nowrap;
    }
</style>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
justingolden21 profile image
Justin Golden

What about simply giving the button a title of "close?" That would be accessible for screen readers and assistive tech right?

Collapse
 
geoffrich profile image
Geoff Rich

If you want to do it using an attribute, aria-label="Close" would be preferable to title, since title is inconsistently announced by screen readers. However, visually hidden text (the technique above) is more likely to be translated.

Thread Thread
 
justingolden21 profile image
Justin Golden

Got it, thank you.

Collapse
 
danawoodman profile image
Dana Woodman

Haha thanks Geoff! Will update the article, good feedback 💯