Toast

    @use 'sprucecss/scss/spruce' as *;
    
    .toast-demo {
      @include layout-cluster('s');
    }
    
    .toast-region {
      inset-block-end: spacer('m');
      inset-inline-end: spacer('m');
      max-inline-size: calc(100vi - #{spacer('l')});
      position: fixed;
      z-index: z-index('toast');
    }
    
    .toast-list {
      @include clear-list;
      display: flex;
      flex-direction: column;
      gap: spacer('xs');
    }
    
    .toast {
      --toast-color: #{color('info', 'alert')};
      align-items: center;
      background-color: color('surface');
      border: config('border-width-base', $display) solid color('border');
      border-radius: radius('md');
      box-shadow: shadow('lg');
      display: flex;
      gap: spacer('s');
      inline-size: 22rem;
      margin: 0 !important;
      max-inline-size: 100%;
      padding: spacer('xs') spacer('xs') spacer('xs') spacer('s');
    
      @include motion-safe {
        animation: toast-in 0.2s ease-out;
      }
    
      &--success {
        --toast-color: #{color('success', 'alert')};
      }
    
      &--danger {
        --toast-color: #{color('danger', 'alert')};
      }
    
      &--info {
        --toast-color: #{color('info', 'alert')};
      }
    
      &__indicator {
        background-color: var(--toast-color);
        block-size: 0.625rem;
        border-radius: 50%;
        box-shadow: 0 0 0 4px color-mix(in srgb, var(--toast-color) 20%, transparent);
        flex-shrink: 0;
        inline-size: 0.625rem;
      }
    
      &__message {
        color: color('heading');
        flex-grow: 1;
        line-height: config('line-height-md', $typography);
        margin: 0;
      }
    
      &.is-leaving {
        opacity: 0;
        translate: 0 0.5rem;
        @include transition($property: 'opacity, translate');
      }
    }
    
    @keyframes toast-in {
      from {
        opacity: 0;
        translate: 0 0.5rem;
      }
    }
    <div class="toast-demo">
        <button class="btn btn--primary" type="button" data-toast="Your changes were saved." data-toast-type="success">Show success toast</button>
        <button class="btn btn--outline-danger" type="button" data-toast="We couldn’t reach the server. Try again." data-toast-type="danger">Show error toast</button>
        <button class="btn btn--ghost-primary" type="button" data-toast="A new version is available." data-toast-type="info">Show info toast</button>
    </div>
    <div class="toast-region" role="region" aria-label="Notifications">
        <ol class="toast-list" aria-live="polite" aria-relevant="additions"></ol>
    </div>
    <template id="toast-template">
        <li class="toast">
            <span class="toast__indicator" aria-hidden="true"></span>
            <p class="toast__message"></p>
            <button class="btn btn--ghost-secondary btn--icon btn--sm toast__close" type="button" aria-label="Dismiss notification">
                <svg class="btn__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M18 6 6 18M6 6l12 12"/></svg>
            </button>
        </li>
    </template>
    (() => {
      const list = document.querySelector('.toast-list');
      const template = document.getElementById('toast-template');
    
      if (!list || !template) {
        return;
      }
    
      const dismiss = (toast) => {
        toast.classList.add('is-leaving');
        setTimeout(() => toast.remove(), 200);
      };
    
      const showToast = (message, type = 'info', duration = 5000) => {
        const toast = template.content.firstElementChild.cloneNode(true);
        let timer = null;
    
        const start = () => { timer = setTimeout(() => dismiss(toast), duration); };
        const stop = () => clearTimeout(timer);
    
        toast.classList.add(`toast--${type}`);
        toast.querySelector('.toast__message').textContent = message;
        toast.querySelector('.toast__close').addEventListener('click', () => dismiss(toast));
        toast.addEventListener('mouseenter', stop);
        toast.addEventListener('mouseleave', start);
        toast.addEventListener('focusin', stop);
        toast.addEventListener('focusout', start);
    
        list.append(toast);
        start();
    
        return toast;
      };
    
      window.showToast = showToast;
    
      document.querySelectorAll('[data-toast]').forEach((button) => {
        button.addEventListener('click', () => showToast(button.dataset.toast, button.dataset.toastType));
      });
    })();

    Toast

    On this page

    Short, non-blocking notifications that appear in a corner and disappear by themselves.

    Technical Details

    • The toasts are rendered into a live region (aria-live="polite"), so screen readers announce them without moving the focus.
    • A <template> holds the toast markup; showToast(message, type) clones it. It is exposed as window.showToast for your own code.
    • Toasts stay for 5 seconds, but the timer pauses while the pointer is over the toast or the focus is inside it (WCAG 2.2.1 Timing Adjustable). The close button dismisses it immediately.
    • Types: success, danger, info; add more with a .toast--name { --toast-color: ...; } rule.
    • The region uses z-index('toast') from the new $z-indexes map.

    Documentation

    Learn about Spruce CSS through our extensive documentation.

    Components

    Explore our UI library of accessible components built with Spruce CSS.

    Blocks and Pages

    Full-width page blocks and complete page templates, ready to copy.

    Find us on GitHub

    Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.