ShardsUI is in beta. APIs may change before 1.0.

Toast

A self-dismissing message.

<script lang="ts">
  import { Toast } from '@shardsui/svelte/toast'

  const manager = new Toast.Manager()

  function saveChanges() {
    manager.add({
      title: 'Changes saved',
      description: 'Your updates were saved.'
    })
  }
</script>

<Toast.Provider toastManager={manager}>
  <button
    type="button"
    class="box-border flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 py-0 text-sm font-normal text-gray-900 outline-0 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
    onclick={saveChanges}>Save changes</button
  >

  <Toast.Portal>
    <Toast.Viewport
      class="fixed top-auto right-4 bottom-4 z-10 mx-auto flex w-62.5 sm:right-8 sm:bottom-8 sm:w-75"
    >
      {#each Toast.getToastManager().toasts as toast (toast.id)}
        <Toast.Root
          {toast}
          class="absolute right-0 bottom-0 left-auto z-[calc(1000-var(--toast-index))] mr-0 h-(--height) w-full origin-bottom transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] rounded-lg bg-gray-50 p-4 shadow-lg outline-1 outline-gray-200 select-none [--gap:0.75rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))] [transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,height_0.15s] after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-[''] data-ending-style:opacity-0 data-expanded:h-(--toast-height) data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--offset-y)))] data-limited:opacity-0 data-starting-style:transform-[translateY(150%)] data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+150%))] data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+150%))] data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-150%))] data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-150%))] [&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:transform-[translateY(150%)]"
        >
          <Toast.Content
            class="overflow-hidden transition-opacity duration-250 ease-out-quint data-behind:pointer-events-none data-behind:opacity-0 data-expanded:pointer-events-auto data-expanded:opacity-100"
          >
            <Toast.Title class="text-sm/5 font-semibold" />
            <Toast.Description class="text-sm/5 text-gray-700" />
            <Toast.Close
              class="absolute top-2 right-2 flex size-5 items-center justify-center rounded-sm border-none bg-transparent text-gray-500 hover:bg-gray-100 hover:text-gray-700"
              aria-label="Close"
            >
              {@render closeIcon()}
            </Toast.Close>
          </Toast.Content>
        </Toast.Root>
      {/each}
    </Toast.Viewport>
  </Toast.Portal>
</Toast.Provider>

{#snippet closeIcon()}
  <svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
    <path
      d="M6.25 6.25L17.75 17.75M17.75 6.25L6.25 17.75"
      stroke="currentColor"
      stroke-width="1.5"
      stroke-linecap="round"
    />
  </svg>
{/snippet}

Anatomy

<script>
  import { Toast } from '@shardsui/svelte/toast'
</script>

<Toast.Provider>
  <Toast.Portal>
    <Toast.Viewport>
      <!-- Stacked toasts -->
      <Toast.Root {toast}>
        <Toast.Content>
          <Toast.Title />
          <Toast.Description />
          <Toast.Action />
          <Toast.Close />
        </Toast.Content>
      </Toast.Root>

      <!-- Anchored toasts -->
      <Toast.Positioner {toast}>
        <Toast.Root {toast}>
          <Toast.Arrow />
          <Toast.Content>
            <Toast.Title />
            <Toast.Description />
            <Toast.Action />
            <Toast.Close />
          </Toast.Content>
        </Toast.Root>
      </Toast.Positioner>
    </Toast.Viewport>
  </Toast.Portal>
</Toast.Provider>

Usage guidelines

  • Mount one provider near the root: toasts belong to the <Toast.Provider> that added them, so a second provider keeps a separate queue and viewport. Use more than one only when you want those queues kept apart.
  • Rename the viewport if "Notifications" doesn't fit: the viewport is a labelled landmark, which is what lets F6 jump focus to it from anywhere. Pass your own aria-label to <Toast.Viewport> to change the name screen readers announce.
  • Exempt your own interactive elements from swiping: button, a, input, textarea and [role="button"] never start a swipe-to-dismiss. Add data-shards-ui-swipe-ignore to anything else that shouldn't.

Global manager

Create a global manager with new Toast.Manager() and pass it to <Toast.Provider>. Any part of the app, including code outside the component tree, can then queue a toast that renders through the same viewport.

The toastManager exposes add, close, update, promise, and subscribe. For a reactive list of the current toasts, read Toast.getToastManager().toasts inside a <Toast.Provider>.

<script>
  import { Toast } from '@shardsui/svelte/toast'

  const toastManager = new Toast.Manager()
</script>

<Toast.Provider {toastManager}>
  {#each Toast.getToastManager().toasts as toast (toast.id)}
    <!-- … -->
  {/each}
</Toast.Provider>

Stacking and animations

Read --toast-index to set each toast's stacking order; index 0 sits at the front.

.toast {
  z-index: calc(1000 - var(--toast-index));
  transform: scale(calc(1 - 0.1 * var(--toast-index)));
}

--toast-offset-y gives each toast its vertical offset when toasts are positioned absolutely and translated apart. Pair it with the data-expanded attribute (present while the viewport is hovered or focused) to spread the stack open.

.toast[data-expanded] {
  transform: translateY(var(--toast-offset-y));
}

While the stack is collapsed, clamp every toast's height to the frontmost toast with --toast-frontmost-height, and let <Toast.Content> hide the content of the toasts behind it. The data-behind attribute flags content behind the frontmost toast; combine it with data-expanded so it fades back in when the viewport expands:

.toast {
  height: var(--toast-frontmost-height, var(--toast-height));
}

.toast-content {
  overflow: hidden;
  transition: opacity 0.25s;
}

.toast-content[data-behind] {
  opacity: 0;
}

.toast-content[data-expanded] {
  opacity: 1;
}

--toast-swipe-movement-x and --toast-swipe-movement-y track how far the current swipe has moved; translate the toast by them so it follows the pointer.

.toast {
  transform: scale(calc(1 - 0.1 * var(--toast-index))) translateX(var(--toast-swipe-movement-x))
    translateY(calc(var(--toast-swipe-movement-y) + (var(--toast-index) * -20%)));
}

On dismissal, the data-swipe-direction attribute reports which way the toast was swiped; use it to fling the toast off-screen in the same direction.

&[data-ending-style] {
  opacity: 0;

  &[data-swipe-direction='up'] {
    transform: translateY(calc(var(--toast-swipe-movement-y) - 150%));
  }
  &[data-swipe-direction='down'] {
    transform: translateY(calc(var(--toast-swipe-movement-y) + 150%));
  }
  /* --offset-y derives locally from --toast-offset-y, --toast-index, and swipe movement */
  &[data-swipe-direction='left'] {
    transform: translateX(calc(var(--toast-swipe-movement-x) - 150%)) translateY(var(--offset-y));
  }
  &[data-swipe-direction='right'] {
    transform: translateX(calc(var(--toast-swipe-movement-x) + 150%)) translateY(var(--offset-y));
  }
}

The data-limited attribute marks a toast that exceeded the limit option. Limited toasts stay mounted with the HTML inert attribute, so you can hide them outright or animate them differently from the visible stack.

updateKey increments every time a toast is updated or upserted; key an animation off it to replay an attention-grabbing effect. When a remount is acceptable, wrap the toast markup in a {#key} block keyed on it instead.

Examples

Anchored toasts

Anchor a toast to a specific element with <Toast.Positioner> and the positionerProps option passed when you add it. Useful for contextual feedback, like a transient "Copied" toast next to the button the user just clicked.

Render anchored toasts in their own <Toast.Provider>, separate from stacked ones. Give each provider its own global manager, and the two can be driven independently from anywhere in the app:

<script>
  import { Toast } from '@shardsui/svelte/toast'

  const anchoredToastManager = new Toast.Manager()
  const stackedToastManager = new Toast.Manager()
</script>

<Toast.Provider toastManager={anchoredToastManager}>
  <AnchoredToasts />
</Toast.Provider>
<Toast.Provider toastManager={stackedToastManager}>
  <StackedToasts />
</Toast.Provider>
<script>
  import { Toast } from '@shardsui/svelte/toast'
</script>

<Toast.Viewport>
  {#each Toast.getToastManager().toasts as toast (toast.id)}
    <Toast.Positioner {toast}>
      <Toast.Root {toast}><!-- … --></Toast.Root>
    </Toast.Positioner>
  {/each}
</Toast.Viewport>

Pass positionerProps when adding the toast. Its type is ToastManagerPositionerProps — the anchor-positioning props Toast.Positioner accepts:

<script>
  let buttonEl = $state(null)

  function onclick() {
    anchoredToastManager.add({
      description: 'Copied',
      timeout: 1500,
      positionerProps: {
        anchor: buttonEl,
        sideOffset: 10
      }
    })
  }
</script>

<button bind:this={buttonEl} {onclick}>Copy</button>
<script lang="ts">
  import { Toast } from '@shardsui/svelte/toast'

  const anchoredManager = new Toast.Manager()

  let copyButtonEl = $state<HTMLElement | null>(null)
  let copied = $state(false)

  function copyLink() {
    if (copied) return
    copied = true
    anchoredManager.add({
      description: 'Link copied',
      timeout: 1500,
      positionerProps: {
        anchor: copyButtonEl,
        sideOffset: 10
      },
      onClose() {
        copied = false
      }
    })
  }
</script>

<Toast.Provider toastManager={anchoredManager}>
  <Toast.Portal>
    <Toast.Viewport class="outline-0">
      {#each Toast.getToastManager().toasts as toast (toast.id)}
        <Toast.Positioner {toast} class="z-[calc(1000-var(--toast-index))]">
          <Toast.Root
            {toast}
            class="group flex w-max origin-(--transform-origin) flex-col rounded-md bg-gray-50 px-2 py-1 text-sm shadow-lg outline-1 outline-gray-200 transition-[transform,scale,opacity] duration-100 ease-out focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0"
          >
            <Toast.Arrow
              class="data-[side=bottom]:-top-2 data-[side=left]:-right-3.25 data-[side=left]:rotate-90 data-[side=right]:-left-3.25 data-[side=right]:-rotate-90 data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
            >
              {@render arrow()}
            </Toast.Arrow>
            <Toast.Content>
              <Toast.Description />
            </Toast.Content>
          </Toast.Root>
        </Toast.Positioner>
      {/each}
    </Toast.Viewport>
  </Toast.Portal>
</Toast.Provider>

<button
  type="button"
  bind:this={copyButtonEl}
  class="box-border flex h-8 items-center gap-2 rounded-md border border-gray-200 bg-gray-50 px-3 py-0 text-sm font-normal text-gray-900 outline-0 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
  onclick={copyLink}
>
  {#if copied}
    {@render checkIcon()}
  {:else}
    {@render clipboardIcon()}
  {/if}
  Copy link
</button>

{#snippet clipboardIcon()}
  <svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
    <path
      d="M16.25 4.75H17.25C18.9069 4.75 20.25 6.09315 20.25 7.75V18.25C20.25 19.9069 18.9069 21.25 17.25 21.25H6.75C5.09315 21.25 3.75 19.9069 3.75 18.25V7.75C3.75 6.09315 5.09315 4.75 6.75 4.75H7.75M8.75 7.25H15.25C15.8023 7.25 16.25 6.80228 16.25 6.25V5.75C16.25 4.09315 14.9069 2.75 13.25 2.75H10.75C9.09315 2.75 7.75 4.09315 7.75 5.75V6.25C7.75 6.80228 8.19772 7.25 8.75 7.25Z"
      stroke="currentColor"
      stroke-width="1.5"
      stroke-linecap="square"
      stroke-linejoin="round"
    />
  </svg>
{/snippet}

{#snippet checkIcon()}
  <svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
    <path
      d="M6 14.15L10.0321 18L18 7"
      stroke="currentColor"
      stroke-width="1.5"
      stroke-linecap="round"
      stroke-linejoin="round"
    />
  </svg>
{/snippet}

{#snippet arrow()}
  <svg width="20" height="10" viewBox="0 0 20 10" fill="none">
    <path
      d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
      class="fill-gray-50"
    />
    <path
      d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
      class="fill-gray-200"
    />
    <path
      d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
    />
  </svg>
{/snippet}

Custom position

Your CSS decides where toasts sit: adjust the Viewport and Root styles to move them. A reusable toast component could accept a data-position attribute and let CSS handle each placement variant. The demo places the stack at bottom-center:

<script lang="ts">
  import { Toast } from '@shardsui/svelte/toast'

  const manager = new Toast.Manager()

  function notify() {
    manager.add({
      title: 'New reply',
      description: 'Jordan replied to your discussion post.'
    })
  }
</script>

<Toast.Provider toastManager={manager}>
  <button
    type="button"
    class="box-border flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 py-0 text-sm font-normal text-gray-900 outline-0 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
    onclick={notify}>Show notification</button
  >

  <Toast.Portal>
    <Toast.Viewport class="fixed inset-x-0 top-auto bottom-4 z-10 mx-auto flex w-full max-w-75">
      {#each Toast.getToastManager().toasts as toast (toast.id)}
        <Toast.Root
          {toast}
          swipeDirection="down"
          class="absolute inset-x-0 bottom-0 z-[calc(1000-var(--toast-index))] mx-auto h-(--height) w-75 origin-bottom transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] rounded-lg bg-gray-50 p-4 shadow-lg outline-1 outline-gray-200 select-none [--gap:0.75rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))] [transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,height_0.15s] after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-[''] data-ending-style:opacity-0 data-expanded:h-(--toast-height) data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--offset-y)))] data-limited:opacity-0 data-starting-style:transform-[translateY(150%)] data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+150%))] data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+150%))] data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-150%))] data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-150%))] [&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:transform-[translateY(150%)]"
        >
          <Toast.Content
            class="overflow-hidden transition-opacity duration-250 data-behind:pointer-events-none data-behind:opacity-0 data-expanded:pointer-events-auto data-expanded:opacity-100"
          >
            <Toast.Title class="text-sm/5 font-semibold" />
            <Toast.Description class="text-sm/5 text-gray-700" />
            <Toast.Close
              class="absolute top-2 right-2 flex size-5 items-center justify-center rounded-sm border-none bg-transparent text-gray-500 hover:bg-gray-100 hover:text-gray-700"
              aria-label="Close"
            >
              {@render closeIcon()}
            </Toast.Close>
          </Toast.Content>
        </Toast.Root>
      {/each}
    </Toast.Viewport>
  </Toast.Portal>
</Toast.Provider>

{#snippet closeIcon()}
  <svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
    <path
      d="M6.25 6.25L17.75 17.75M17.75 6.25L6.25 17.75"
      stroke="currentColor"
      stroke-width="1.5"
      stroke-linecap="round"
    />
  </svg>
{/snippet}

Undo action

Pass the actionProps option when adding a toast to configure an action button inside it, such as an Undo button.

<script lang="ts">
  import { Toast } from '@shardsui/svelte/toast'

  const manager = new Toast.Manager()

  function deleteItem() {
    const id = manager.add({
      title: 'Item deleted',
      description: 'It was removed from your list.',
      timeout: 10000,
      actionProps: {
        children: 'Undo',
        onclick() {
          manager.close(id)
          manager.add({ title: 'Item restored' })
        }
      }
    })
  }
</script>

<Toast.Provider toastManager={manager}>
  <button
    type="button"
    class="box-border flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 py-0 text-sm font-normal text-gray-900 outline-0 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
    onclick={deleteItem}>Delete item</button
  >

  <Toast.Portal>
    <Toast.Viewport
      class="fixed top-auto right-4 bottom-4 z-10 mx-auto flex w-62.5 sm:right-8 sm:bottom-8 sm:w-75"
    >
      {#each Toast.getToastManager().toasts as toast (toast.id)}
        <Toast.Root
          {toast}
          class="absolute right-0 bottom-0 left-auto z-[calc(1000-var(--toast-index))] mr-0 h-(--height) w-full origin-bottom transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] rounded-lg bg-gray-50 p-4 shadow-lg outline-1 outline-gray-200 select-none [--gap:0.75rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))] [transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,height_0.15s] after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-[''] data-ending-style:opacity-0 data-expanded:h-(--toast-height) data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--offset-y)))] data-limited:opacity-0 data-starting-style:transform-[translateY(150%)] data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+150%))] data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+150%))] data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-150%))] data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-150%))] [&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:transform-[translateY(150%)]"
        >
          <Toast.Content
            class="overflow-hidden transition-opacity duration-250 data-behind:pointer-events-none data-behind:opacity-0 data-expanded:pointer-events-auto data-expanded:opacity-100"
          >
            <Toast.Title class="text-sm/5 font-semibold" />
            <Toast.Description class="text-sm/5 text-gray-700" />
            <Toast.Action
              class="mt-2 inline-flex h-8 items-center justify-center rounded border-none bg-gray-900 px-3 text-sm/5 font-normal text-gray-50 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950"
            />
          </Toast.Content>
        </Toast.Root>
      {/each}
    </Toast.Viewport>
  </Toast.Portal>
</Toast.Provider>

Promise

An async toast moves through loading, success, and error states; its type string reflects the current one, so you can style each state differently. Each state accepts a plain string or the same options object as the update method to configure that state's toast in full.

<script lang="ts">
  import { Toast } from '@shardsui/svelte/toast'

  const manager = new Toast.Manager()

  function upload() {
    manager
      .promise(
        new Promise<string>((resolve, reject) => {
          setTimeout(() => {
            if (Math.random() > 0.3) {
              resolve('design-tokens.json')
            } else {
              reject(new Error('connection lost'))
            }
          }, 2000)
        }),
        {
          loading: 'Uploading tokens…',
          success: (name: string) => `Uploaded ${name}`,
          error: (err) => `Upload failed: ${(err as Error).message}`
        }
      )
      .catch(() => {})
  }
</script>

<Toast.Provider toastManager={manager}>
  <button
    type="button"
    class="box-border flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 py-0 text-sm font-normal text-gray-900 outline-0 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
    onclick={upload}>Upload tokens</button
  >

  <Toast.Portal>
    <Toast.Viewport
      class="fixed top-auto right-4 bottom-4 z-10 mx-auto flex w-62.5 sm:right-8 sm:bottom-8 sm:w-75"
    >
      {#each Toast.getToastManager().toasts as toast (toast.id)}
        <Toast.Root
          {toast}
          class="absolute right-0 bottom-0 left-auto z-[calc(1000-var(--toast-index))] mr-0 h-(--height) w-full origin-bottom transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] rounded-lg bg-gray-50 p-4 shadow-lg outline-1 outline-gray-200 select-none [--gap:0.75rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))] [transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,height_0.15s] after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-[''] data-ending-style:opacity-0 data-expanded:h-(--toast-height) data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--offset-y)))] data-limited:opacity-0 data-starting-style:transform-[translateY(150%)] data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+150%))] data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+150%))] data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-150%))] data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-150%))] data-[type=error]:border-red-200 data-[type=error]:bg-red-50 data-[type=error]:text-red-950 data-[type=success]:border-green-200 data-[type=success]:bg-green-50 data-[type=success]:text-green-950 [&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:transform-[translateY(150%)]"
        >
          <Toast.Content
            class="overflow-hidden transition-opacity duration-250 data-behind:pointer-events-none data-behind:opacity-0 data-expanded:pointer-events-auto data-expanded:opacity-100"
          >
            <Toast.Title class="text-sm/5 font-semibold" />
            <Toast.Description class="text-sm/5 text-gray-700" />
            <Toast.Close
              class="absolute top-2 right-2 flex size-5 items-center justify-center rounded-sm border-none bg-transparent text-gray-500 hover:bg-gray-100 hover:text-gray-700"
              aria-label="Close"
            >
              {@render closeIcon()}
            </Toast.Close>
          </Toast.Content>
        </Toast.Root>
      {/each}
    </Toast.Viewport>
  </Toast.Portal>
</Toast.Provider>

{#snippet closeIcon()}
  <svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
    <path
      d="M6.25 6.25L17.75 17.75M17.75 6.25L6.25 17.75"
      stroke="currentColor"
      stroke-width="1.5"
      stroke-linecap="round"
    />
  </svg>
{/snippet}

Custom

Attach arbitrary typed data, values or functions alike, to a toast through the data option.

<script lang="ts">
  import { Toast } from '@shardsui/svelte/toast'
  import type { ToastObject } from '@shardsui/svelte/toast'

  type OrderData = {
    orderId: string
  }

  function hasOrder(toast: ToastObject): toast is ToastObject<OrderData> {
    return (toast.data as OrderData | undefined)?.orderId !== undefined
  }

  const manager = new Toast.Manager()

  function submitOrder() {
    manager.add({
      title: 'Order submitted',
      data: { orderId: 'ORD-204' } satisfies OrderData
    })
  }
</script>

<Toast.Provider toastManager={manager}>
  <button
    type="button"
    class="box-border flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 py-0 text-sm font-normal text-gray-900 outline-0 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
    onclick={submitOrder}>Submit order</button
  >

  <Toast.Portal>
    <Toast.Viewport
      class="fixed top-auto right-4 bottom-4 z-10 mx-auto flex w-62.5 sm:right-8 sm:bottom-8 sm:w-75"
    >
      {#each Toast.getToastManager().toasts as toast (toast.id)}
        <Toast.Root
          {toast}
          class="absolute right-0 bottom-0 left-auto z-[calc(1000-var(--toast-index))] mr-0 h-(--height) w-full origin-bottom transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] rounded-lg bg-gray-50 p-4 shadow-lg outline-1 outline-gray-200 select-none [--gap:0.75rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))] [transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,height_0.15s] after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-[''] data-ending-style:opacity-0 data-expanded:h-(--toast-height) data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--offset-y)))] data-limited:opacity-0 data-starting-style:transform-[translateY(150%)] data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+150%))] data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+150%))] data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-150%))] data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-150%))] [&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:transform-[translateY(150%)]"
        >
          <Toast.Content
            class="overflow-hidden transition-opacity duration-250 data-behind:pointer-events-none data-behind:opacity-0 data-expanded:pointer-events-auto data-expanded:opacity-100"
          >
            <Toast.Title class="text-sm/5 font-semibold" />
            {#if hasOrder(toast)}
              <Toast.Description class="text-sm/5 text-gray-700">
                View order {toast.data?.orderId}.
              </Toast.Description>
            {/if}
            <Toast.Close
              class="absolute top-2 right-2 flex size-5 items-center justify-center rounded-sm border-none bg-transparent text-gray-500 hover:bg-gray-100 hover:text-gray-700"
              aria-label="Close"
            >
              {@render closeIcon()}
            </Toast.Close>
          </Toast.Content>
        </Toast.Root>
      {/each}
    </Toast.Viewport>
  </Toast.Portal>
</Toast.Provider>

{#snippet closeIcon()}
  <svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
    <path
      d="M6.25 6.25L17.75 17.75M17.75 6.25L6.25 17.75"
      stroke="currentColor"
      stroke-width="1.5"
      stroke-linecap="round"
    />
  </svg>
{/snippet}

Deduplicated toast

Upserting a toast by the same id bumps its updateKey, letting a custom renderer replay an animation. Below, alternating CSS animation names off updateKey keeps the toast mounted while re-triggering the pulse.

<script lang="ts">
  import { Toast } from '@shardsui/svelte/toast'

  const manager = new Toast.Manager()

  function bookmarkItem() {
    manager.add({
      id: 'bookmark',
      title: 'Bookmarked',
      description: 'Click again while it is visible to replay the pulse.'
    })
  }
</script>

<Toast.Provider toastManager={manager}>
  <button
    type="button"
    class="box-border flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 py-0 text-sm font-normal text-gray-900 outline-0 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
    onclick={bookmarkItem}>Bookmark</button
  >

  <Toast.Portal>
    <Toast.Viewport
      class="fixed top-auto right-4 bottom-4 z-10 mx-auto flex w-62.5 sm:right-8 sm:bottom-8 sm:w-75"
    >
      {#each Toast.getToastManager().toasts as toast (toast.id)}
        {@const pulseStyle = toast.updateKey
          ? `animation:${toast.updateKey % 2 === 0 ? 'pulse-even' : 'pulse-odd'} 0.28s ease;`
          : ''}
        <Toast.Root
          {toast}
          style={pulseStyle}
          class="absolute right-0 bottom-0 left-auto z-[calc(1000-var(--toast-index))] mr-0 h-(--height) w-full origin-bottom transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] rounded-lg bg-gray-50 p-4 shadow-lg outline-1 outline-gray-200 select-none [--gap:0.75rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))] [transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,height_0.15s] after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-[''] data-ending-style:opacity-0 data-expanded:h-(--toast-height) data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--offset-y)))] data-limited:opacity-0 data-starting-style:transform-[translateY(150%)] data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+150%))] data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+150%))] data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-150%))] data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-150%))] [&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:transform-[translateY(150%)]"
        >
          <Toast.Content
            class="overflow-hidden transition-opacity duration-250 data-behind:pointer-events-none data-behind:opacity-0 data-expanded:pointer-events-auto data-expanded:opacity-100"
          >
            <Toast.Title class="text-sm/5 font-semibold" />
            <Toast.Description class="text-sm/5 text-gray-700" />
            <Toast.Close
              class="absolute top-2 right-2 flex size-5 items-center justify-center rounded-sm border-none bg-transparent text-gray-500 hover:bg-gray-100 hover:text-gray-700"
              aria-label="Close"
            >
              {@render closeIcon()}
            </Toast.Close>
          </Toast.Content>
        </Toast.Root>
      {/each}
    </Toast.Viewport>
  </Toast.Portal>
</Toast.Provider>

{#snippet closeIcon()}
  <svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
    <path
      d="M6.25 6.25L17.75 17.75M17.75 6.25L6.25 17.75"
      stroke="currentColor"
      stroke-width="1.5"
      stroke-linecap="round"
    />
  </svg>
{/snippet}

<style>
  /* Two identical keyframes on purpose: alternating the name restarts the pulse without a remount. */
  @keyframes -global-pulse-even {
    0%,
    100% {
      scale: 1;
    }
    50% {
      scale: 1.04;
    }
  }
  @keyframes -global-pulse-odd {
    0%,
    100% {
      scale: 1;
    }
    50% {
      scale: 1.04;
    }
  }
</style>

Varying heights

To stack toasts of different heights cleanly, clamp every toast's height to the frontmost one at index 0 with the --toast-frontmost-height CSS variable, while the data-behind attribute hides the content of the toasts behind it. Avoid sizing <Toast.Content> to the root's height (such as height: 100%) — resizing it alongside the root cancels the root's height transition.

<script lang="ts">
  import { Toast } from '@shardsui/svelte/toast'

  const messages = [
    { title: 'Link copied', description: 'Copied to your clipboard.' },
    {
      title: 'Backup complete',
      description:
        'All of your files were backed up to the cloud. You can restore any version from the last 30 days whenever you need it.'
    },
    { title: 'Note added', description: 'Saved to your notes.' }
  ]

  const manager = new Toast.Manager()
  let next = 0

  function createToast() {
    manager.add(messages[next % messages.length])
    next++
  }
</script>

<Toast.Provider toastManager={manager}>
  <button
    type="button"
    class="box-border flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 py-0 text-sm font-normal text-gray-900 outline-0 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
    onclick={createToast}>Create toast</button
  >

  <Toast.Portal>
    <Toast.Viewport
      class="fixed top-auto right-4 bottom-4 z-10 mx-auto flex w-62.5 sm:right-8 sm:bottom-8 sm:w-75"
    >
      {#each Toast.getToastManager().toasts as toast (toast.id)}
        <Toast.Root
          {toast}
          class="absolute right-0 bottom-0 left-auto z-[calc(1000-var(--toast-index))] mr-0 h-(--height) w-full origin-bottom transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] rounded-lg bg-gray-50 p-4 shadow-lg outline-1 outline-gray-200 select-none [--gap:0.75rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))] [transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,height_0.15s] after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-[''] data-ending-style:opacity-0 data-expanded:h-(--toast-height) data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--offset-y)))] data-limited:opacity-0 data-starting-style:transform-[translateY(150%)] data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+150%))] data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+150%))] data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-150%))] data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-150%))] [&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:transform-[translateY(150%)]"
        >
          <Toast.Content
            class="overflow-hidden transition-opacity duration-250 data-behind:pointer-events-none data-behind:opacity-0 data-expanded:pointer-events-auto data-expanded:opacity-100"
          >
            <Toast.Title class="text-sm/5 font-semibold" />
            <Toast.Description class="text-sm/5 text-gray-700" />
            <Toast.Close
              class="absolute top-2 right-2 flex size-5 items-center justify-center rounded-sm border-none bg-transparent text-gray-500 hover:bg-gray-100 hover:text-gray-700"
              aria-label="Close"
            >
              {@render closeIcon()}
            </Toast.Close>
          </Toast.Content>
        </Toast.Root>
      {/each}
    </Toast.Viewport>
  </Toast.Portal>
</Toast.Provider>

{#snippet closeIcon()}
  <svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
    <path
      d="M6.25 6.25L17.75 17.75M17.75 6.25L6.25 17.75"
      stroke="currentColor"
      stroke-width="1.5"
      stroke-linecap="round"
    />
  </svg>
{/snippet}

API reference

Provider

Provides a context for creating and managing toasts. Doesn't render its own HTML element.

PropTypeDefault

Portal

A portal that moves the viewport out to <body>, clear of ancestor clipping and stacking. Renders a <div> element.

PropTypeDefault

Viewport

A container viewport for toasts. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-expandedPresent when toasts are expanded (hovering or focused).
CSS VariableDescription
--toast-frontmost-heightThe height of the frontmost toast.

Root

Groups all parts of an individual toast. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-expandedPresent when the toast viewport is expanded.
data-limitedPresent when removed due to exceeding the limit.
data-typeThe type of the toast (string).
data-swipingPresent when the toast is being swiped.
data-swipe-directionThe direction of the swipe ('up' | 'down' | 'left' | 'right').
data-starting-stylePresent when the toast is animating in.
data-ending-stylePresent when the toast is animating out.
CSS VariableDescription
--toast-indexThe index of the toast in the list.
--toast-offset-yThe vertical offset when expanded (px).
--toast-heightThe measured natural height (px).
--toast-swipe-movement-xHorizontal swipe movement (px).
--toast-swipe-movement-yVertical swipe movement (px).

Content

A container for the contents of a toast. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-expandedPresent when the toast viewport is expanded.
data-behindPresent when behind the frontmost toast.

Title

A title that labels the toast. Renders an <h2> element.

PropTypeDefault
AttributeDescription
data-typeThe type of the toast (string).

Description

Secondary text for the toast. Can be used as the default message for the toast when no title is provided. Renders a <p> element.

PropTypeDefault
AttributeDescription
data-typeThe type of the toast (string).

Action

Performs an action when clicked. Renders a <button> element.

PropTypeDefault
AttributeDescription
data-typeThe type of the toast (string).

Close

Closes the toast when clicked. Renders a <button> element.

PropTypeDefault
AttributeDescription
data-typeThe type of the toast (string).

Positioner

Positions the toast against the anchor. Renders a <div> element.

PropTypeDefault

Props can also be passed via toast.positionerProps when calling toastManager.add().

AttributeDescription
data-sideWhich side of the anchor the popup is on.
data-alignHow the popup is aligned relative to the side.
data-anchor-hiddenPresent when the anchor is hidden.
CSS VariableDescription
--available-widthAvailable width between the anchor and the viewport edge.
--available-heightAvailable height between the anchor and the viewport edge.
--anchor-widthWidth of the anchor element.
--anchor-heightHeight of the anchor element.
--transform-originTransform origin for scale animations.
--toast-indexIndex of this toast in the stack.

Arrow

Displays an element positioned against the anchor. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-sideWhich side of the anchor the popup is on.
data-alignHow the popup is aligned relative to the side.
data-uncenteredPresent when the arrow cannot be centered.

Toast.getToastManager

Manages toasts; call it inside a <Toast.Provider>.

<script>
  import { Toast } from '@shardsui/svelte/toast'

  const toastManager = Toast.getToastManager()
</script>

Returns { toasts, add, close, update, promise }.

add method

Adds a toast to the list and returns its toastId, which you can later hand to update or close. Reuse an existing id and the matching toast is updated in place rather than duplicated.

const toastId = toastManager.add({
  title: 'Hello',
  description: 'Hello, world!'
})

For high-priority toasts (priority: 'high'), screen readers announce the title and description strings through a hidden role="alert" live region. Other markup inside <Toast.Root>, including the <Toast.Title> and <Toast.Description> components, stays silent unless the user navigates into the toast viewport.

update method

Updates the toast with new options.

toastManager.update(toastId, {
  description: 'New description'
})

close method

Closes the toast, removing it from the toast list after any animations complete.

toastManager.close(toastId) // Close one
toastManager.close() // Close all

promise method

Creates an asynchronous toast with three possible states: loading, success, and error.

toastManager.promise(fetch('/api/data'), {
  loading: 'Loading…',
  success: (data) => `Loaded ${data.length} items`,
  error: (err) => `Error: ${err.message}`
})