Beta, expect API change, and bugs. Hit one? Tell us.

Skip to content

Toast

A self-dismissing message.

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

const manager = Toast.createManager()

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

<template>
  <Toast.Provider v-slot="{ toasts }" :toast-manager="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"
      @click="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"
      >
        <Toast.Root
          v-for="toast in toasts"
          :key="toast.id"
          :toast="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"
            >
              <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>
            </Toast.Close>
          </Toast.Content>
        </Toast.Root>
      </Toast.Viewport>
    </Toast.Portal>
  </Toast.Provider>
</template>

Anatomy

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

<template>
  <Toast.Provider>
    <Toast.Portal>
      <Toast.Viewport>
        <!-- Stacked toasts -->
        <Toast.Root :toast="toast">
          <Toast.Content>
            <Toast.Title />
            <Toast.Description />
            <Toast.Action />
            <Toast.Close />
          </Toast.Content>
        </Toast.Root>

        <!-- Anchored toasts -->
        <Toast.Positioner :toast="toast">
          <Toast.Root :toast="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>
</template>

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 Toast.createManager() 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 the toasts slot prop of <Toast.Provider>, or call Toast.getToastManager().toasts from a component rendered inside it.

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

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

<template>
  <Toast.Provider v-slot="{ toasts }" :toast-manager="toastManager">
    <template v-for="toast in toasts" :key="toast.id">
      <!-- … -->
    </template>
  </Toast.Provider>
</template>

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 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. Combine data-behind 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, use data-swipe-direction to fling the toast off-screen in the direction it was swiped.

&[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));
  }
}

A toast that exceeds the limit option gets data-limited and stays mounted with the HTML inert attribute, so you can hide it outright or animate it differently from the visible stack.

updateKey increments every time a toast is updated or upserted; key an animation off it to replay an effect. When a remount is acceptable, put it in the :key of the toast markup 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 setup>
import { Toast } from '@shardsui/vue/toast'

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

<template>
  <Toast.Provider :toast-manager="anchoredToastManager">
    <AnchoredToasts />
  </Toast.Provider>
  <Toast.Provider :toast-manager="stackedToastManager">
    <StackedToasts />
  </Toast.Provider>
</template>
<script setup>
import { Toast } from '@shardsui/vue/toast'

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

<template>
  <Toast.Viewport>
    <Toast.Positioner v-for="toast in toastManager.toasts" :key="toast.id" :toast="toast">
      <Toast.Root :toast="toast"><!-- … --></Toast.Root>
    </Toast.Positioner>
  </Toast.Viewport>
</template>

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

<script setup>
import { useTemplateRef } from 'vue'

const button = useTemplateRef('button')

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

<template>
  <button ref="button" @click="copy">Copy</button>
</template>
<script setup lang="ts">
import { Toast } from '@shardsui/vue/toast'
import { shallowRef, useTemplateRef } from 'vue'

const anchoredManager = Toast.createManager()

const copyButton = useTemplateRef<HTMLButtonElement>('copyButton')
const copied = shallowRef(false)

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

<template>
  <Toast.Provider v-slot="{ toasts }" :toast-manager="anchoredManager">
    <Toast.Portal>
      <Toast.Viewport class="outline-0">
        <Toast.Positioner
          v-for="toast in toasts"
          :key="toast.id"
          :toast="toast"
          class="z-[calc(1000-var(--toast-index))]"
        >
          <Toast.Root
            :toast="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"
            >
              <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>
            </Toast.Arrow>
            <Toast.Content>
              <Toast.Description />
            </Toast.Content>
          </Toast.Root>
        </Toast.Positioner>
      </Toast.Viewport>
    </Toast.Portal>
  </Toast.Provider>

  <button
    ref="copyButton"
    type="button"
    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"
    @click="copyLink"
  >
    <svg v-if="copied" 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>
    <svg v-else 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>
    Copy link
  </button>
</template>

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 setup lang="ts">
import { Toast } from '@shardsui/vue/toast'

const manager = Toast.createManager()

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

<template>
  <Toast.Provider v-slot="{ toasts }" :toast-manager="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"
      @click="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">
        <Toast.Root
          v-for="toast in toasts"
          :key="toast.id"
          :toast="toast"
          swipe-direction="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"
            >
              <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>
            </Toast.Close>
          </Toast.Content>
        </Toast.Root>
      </Toast.Viewport>
    </Toast.Portal>
  </Toast.Provider>
</template>

Undo action

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

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

const manager = Toast.createManager()

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>

<template>
  <Toast.Provider v-slot="{ toasts }" :toast-manager="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"
      @click="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"
      >
        <Toast.Root
          v-for="toast in toasts"
          :key="toast.id"
          :toast="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>
      </Toast.Viewport>
    </Toast.Portal>
  </Toast.Provider>
</template>

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 setup lang="ts">
import { Toast } from '@shardsui/vue/toast'

const manager = Toast.createManager()

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>

<template>
  <Toast.Provider v-slot="{ toasts }" :toast-manager="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"
      @click="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"
      >
        <Toast.Root
          v-for="toast in toasts"
          :key="toast.id"
          :toast="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"
            >
              <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>
            </Toast.Close>
          </Toast.Content>
        </Toast.Root>
      </Toast.Viewport>
    </Toast.Portal>
  </Toast.Provider>
</template>

Custom

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

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

type OrderData = {
  orderId: string
}

function hasOrder(toast: ToastObject): toast is ToastObject<OrderData> {
  return toast.data != null && 'orderId' in toast.data
}

const manager = Toast.createManager()

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

<template>
  <Toast.Provider v-slot="{ toasts }" :toast-manager="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"
      @click="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"
      >
        <Toast.Root
          v-for="toast in toasts"
          :key="toast.id"
          :toast="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 v-if="hasOrder(toast)" class="text-sm/5 text-gray-700">
              View order {{ toast.data?.orderId }}.
            </Toast.Description>
            <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"
            >
              <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>
            </Toast.Close>
          </Toast.Content>
        </Toast.Root>
      </Toast.Viewport>
    </Toast.Portal>
  </Toast.Provider>
</template>

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 setup lang="ts">
import { Toast } from '@shardsui/vue/toast'

const manager = Toast.createManager()

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

function pulseStyle(updateKey: number | undefined) {
  return updateKey
    ? `animation:${updateKey % 2 === 0 ? 'pulse-even' : 'pulse-odd'} 0.28s ease;`
    : ''
}
</script>

<template>
  <Toast.Provider v-slot="{ toasts }" :toast-manager="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"
      @click="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"
      >
        <Toast.Root
          v-for="toast in toasts"
          :key="toast.id"
          :toast="toast"
          :style="pulseStyle(toast.updateKey)"
          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"
            >
              <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>
            </Toast.Close>
          </Toast.Content>
        </Toast.Root>
      </Toast.Viewport>
    </Toast.Portal>
  </Toast.Provider>
</template>

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

Varying heights

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 setup lang="ts">
import { Toast } from '@shardsui/vue/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 = Toast.createManager()
let next = 0

function createToast() {
  const message = messages[next % messages.length]
  if (!message) return
  manager.add(message)
  next++
}
</script>

<template>
  <Toast.Provider v-slot="{ toasts }" :toast-manager="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"
      @click="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"
      >
        <Toast.Root
          v-for="toast in toasts"
          :key="toast.id"
          :toast="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"
            >
              <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>
            </Toast.Close>
          </Toast.Content>
        </Toast.Root>
      </Toast.Viewport>
    </Toast.Portal>
  </Toast.Provider>
</template>

API reference

Provider

Provides the toast queue to the parts beneath it. 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 setup>
import { Toast } from '@shardsui/vue/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}`
})