MagicToast
MagicToast let’s you trigger and display toasts from anywhere.
<template>
<m-button @click="onClick">Add Toast</m-button>
<magic-toast-provider id="magic-toast-demo" />
</template>
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue'
import { MButton } from 'mirror-compat'
import { useMagicToast } from '@maas/vue-equipment/plugins/MagicToast'
const count = ref(0)
const component = defineAsyncComponent(
() => import('./components/DemoToast.vue')
)
const { add } = useMagicToast('magic-toast-demo')
function onClick() {
count.value += 1
const props = {
message: count.value % 2 ? 'Magic as a Service™' : 'Vue Equipment',
}
add({ component, props })
}
</script>Anatomy
<template>
<magic-toast-provider id="your-toast-id" />
</template>
<script>
import { defineAsyncComponent } from 'vue'
import { useMagicToast } from '@maas/vue-equipment/plugins/MagicToast'
const component = defineAsyncComponent(() => import('./YourToast.vue'))
const { add } = useMagicToast('your-toast-id')
function handleClick() {
add({ component })
}
</script>
Installation
CLI
Add @maas/vue-equipment to your dependencies.
pnpm add @maas/vue-equipment
Vue
If you are using Vue, import and add MagicToastPlugin to your app.
import { createApp } from 'vue'
import { MagicToastPlugin } from '@maas/vue-equipment/plugins/MagicToast'
const app = createApp({})
app.use(MagicToastPlugin)
Nuxt
The toaster is available as a Nuxt module. In your Nuxt config file add @maas/vue-equipment/nuxt to your modules and add MagicToast to the plugins in your configuration.
export default defineNuxtConfig({
modules: ['@maas/vue-equipment/nuxt'],
vueEquipment: {
plugins: ['MagicToast'],
},
})
Composable
In order to interact with the toaster from anywhere within your app, we provide a useMagicToaster composable. Import it directly when needed.
const component = defineAsyncComponent(() => import('./YourToast.vue'))
const { add } = useMagicToast('your-toast-id')
function handleClick() {
add({ component })
}
Peer Dependencies
MagicToast has no optional peer dependencies. It only relies on the core peer dependencies, which npm, pnpm and bun install automatically.
API Reference
MagicToastProvider
The MagicToastProvider wraps the toaster and configures it according to the provided options.
Props
Options
CSS Variables
MagicToastView
Renders a single toast inside the toaster. Used internally.
useMagicToast
Interact with a toaster instance from anywhere in your app. Pass the same id used on the corresponding MagicToastProvider.
const {
add,
remove,
clear,
hide,
show,
expand,
collapse,
toasts,
count,
hidden,
} = useMagicToast('your-toast-id')
add
Adds a toast to the stack and returns its id.
const id = add({ component, props: { message: 'Saved!' } })
remove
Removes a toast by id.
remove(id)
clear
Removes all toasts, including any currently hidden via hide(). Optionally pass a transition name to use while clearing, instead of the toaster’s configured transition option.
clear()
clear('my-clear-transition')
hide / show
Temporarily hide the entire toast stack and restore it later, preserving each toast’s state (props, timers, etc.) in between. Auto-dismiss timers are paused while hidden and resume when shown again. Both optionally take a transition name to use instead of the toaster’s configured transition option. Calling hide() with nothing to hide, or show() with nothing hidden, is a no-op.
Calling add() while the stack is hidden reveals it again immediately, so a new toast is never silently queued out of sight — it’s equivalent to calling show() first.
hide()
show()
hide('my-hide-transition')
show('my-show-transition')
expand / collapse
Expand or collapse the toast stack, mirroring what layout.expand: 'click' | 'hover' does on interaction.
expand()
collapse()
State
Events
The toaster emits the following events through MagicEmitter. Listen to them with useMagicEmitter.
Errors
Examples
Position
Set the position option to anchor the toaster to any edge or corner of the viewport.
<template>
<div class="flex w-full gap-2">
<m-button @click="onClick">Add Toast</m-button>
<m-select
v-model="position"
:options="options"
label="Position"
placeholder="Position"
/>
</div>
<magic-toast-provider
id="magic-toast-position-demo"
:options="{
position: selected?.value,
}"
/>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, ref } from 'vue'
import { MButton, MSelect } from 'mirror-compat'
import { useMagicToast } from '@maas/vue-equipment/plugins/MagicToast'
import type { MagicToastOptions } from '@maas/vue-equipment/plugins/MagicToast'
type ToastPosition = NonNullable<MagicToastOptions['position']>
const component = defineAsyncComponent(
() => import('./components/DemoToast.vue')
)
const { add } = useMagicToast('magic-toast-position-demo')
const position = ref<string | number>('bottom-right')
const options: Array<{ value: ToastPosition; label: string }> = [
{ value: 'top-right', label: 'Top Right' },
{ value: 'top', label: 'Top' },
{ value: 'top-left', label: 'Top Left' },
{ value: 'left', label: 'Left' },
{ value: 'bottom-left', label: 'Bottom Left' },
{ value: 'bottom', label: 'Bottom' },
{ value: 'bottom-right', label: 'Bottom Right' },
{ value: 'right', label: 'Right' },
]
const selected = computed(() =>
options.find((option) => option.value === position.value)
)
function onClick() {
const props = {
message: selected.value?.label,
}
add({ component, props })
}
</script>Expanded
Set initial.expanded: true together with layout.expand: false to always render the stack fully expanded, instead of collapsed into an overlapping pile.
<template>
<m-button @click="onClick">Add Toast</m-button>
<magic-toast-provider id="magic-toast-expanded-demo" :options="options" />
</template>
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue'
import { MButton } from 'mirror-compat'
import { useMagicToast } from '@maas/vue-equipment/plugins/MagicToast'
import type { MagicToastOptions } from '@maas/vue-equipment/plugins/MagicToast'
const count = ref(0)
const component = defineAsyncComponent(
() => import('./components/DemoToast.vue')
)
const { add } = useMagicToast('magic-toast-expanded-demo')
function onClick() {
count.value += 1
const props = {
message: count.value % 2 ? 'Magic as a Service™' : 'Vue Equipment',
}
add({ component, props })
}
const options: MagicToastOptions = {
layout: {
expand: false,
},
initial: {
expanded: true,
},
}
</script>Collapsed
Set layout.expand: false to keep the stack collapsed permanently, with no click or hover interaction to expand it.
<template>
<m-button @click="onClick">Add Toast</m-button>
<magic-toast-provider id="magic-toast-collapsed-demo" :options="options" />
</template>
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue'
import { MButton } from 'mirror-compat'
import { useMagicToast } from '@maas/vue-equipment/plugins/MagicToast'
import type { MagicToastOptions } from '@maas/vue-equipment/plugins/MagicToast'
const count = ref(0)
const component = defineAsyncComponent(
() => import('./components/DemoToast.vue')
)
const { add } = useMagicToast('magic-toast-collapsed-demo')
function onClick() {
count.value += 1
const props = {
message: count.value % 2 ? 'Magic as a Service™' : 'Vue Equipment',
}
add({ component, props })
}
const options: MagicToastOptions = {
layout: {
expand: false,
},
}
</script>Hover
Set layout.expand: 'hover' to expand the stack while the pointer is over it, and collapse it again on mouse leave.
<template>
<m-button @click="onClick">Add Toast</m-button>
<magic-toast-provider id="magic-toast-hover-demo" :options="options" />
</template>
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue'
import { MButton } from 'mirror-compat'
import { useMagicToast } from '@maas/vue-equipment/plugins/MagicToast'
import type { MagicToastOptions } from '@maas/vue-equipment/plugins/MagicToast'
const count = ref(0)
const component = defineAsyncComponent(
() => import('./components/DemoToast.vue')
)
const { add } = useMagicToast('magic-toast-hover-demo')
function onClick() {
count.value += 1
const props = {
message: count.value % 2 ? 'Magic as a Service™' : 'Vue Equipment',
}
add({ component, props })
}
const options: MagicToastOptions = {
layout: {
expand: 'hover',
},
}
</script>No Drag
Set drag: { disabled: true } on the provider to disable drag-to-dismiss for every toast in the stack.
<template>
<m-button @click="onClick">Add Toast</m-button>
<magic-toast-provider id="magic-toast-no-drag-demo" :options="options" />
</template>
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue'
import { MButton } from 'mirror-compat'
import { useMagicToast } from '@maas/vue-equipment/plugins/MagicToast'
const count = ref(0)
const component = defineAsyncComponent(
() => import('./components/DemoToast.vue')
)
const { add } = useMagicToast('magic-toast-no-drag-demo')
function onClick() {
count.value += 1
const props = {
message: count.value % 2 ? 'Magic as a Service™' : 'Vue Equipment',
}
add({ component, props })
}
const options = {
drag: { disabled: true },
}
</script>Per-Toast Options
Pass options to add() to override provider defaults for a single toast, without affecting the rest of the stack. It accepts the per-toast subset of the toaster options — duration, drag, threshold and animation.
<template>
<div class="grid w-full grid-cols-2 items-end gap-3 sm:grid-cols-3">
<m-toggle v-model="draggable" label="Draggable" />
<m-select
v-model="direction"
:options="directionOptions"
label="Drag direction"
placeholder="Drag direction"
/>
<m-input v-model="duration" type="number" label="Auto-dismiss (ms)" />
<m-input v-model="distance" type="number" label="Dismiss distance (px)" />
<m-input v-model="snapDuration" type="number" label="Snap duration (ms)" />
<m-button class="w-full" @click="onClick">Add Toast</m-button>
</div>
<magic-toast-provider
:id="id"
:options="{
position: 'bottom-right',
layout: { expand: false, max: 10 },
initial: { expanded: true },
}"
/>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, shallowRef } from 'vue'
import { MButton, MInput, MSelect, MToggle } from 'mirror-compat'
import { useMagicToast } from '@maas/vue-equipment/plugins/MagicToast'
import type { MagicToastAddOptions } from '@maas/vue-equipment/plugins/MagicToast'
const id = 'magic-toast-per-toast-options-demo'
const component = defineAsyncComponent(
() => import('./components/DemoToast.vue')
)
const { add } = useMagicToast(id)
const draggable = shallowRef(true)
const duration = shallowRef(0)
const distance = shallowRef(32)
const snapDuration = shallowRef(300)
const direction = shallowRef('auto')
const directionOptions = [
{ value: 'auto', label: 'Auto' },
{ value: 'left', label: 'Left' },
{ value: 'right', label: 'Right' },
{ value: 'up', label: 'Up' },
{ value: 'down', label: 'Down' },
{ value: 'left-right', label: 'Left & Right' },
{ value: 'up-down', label: 'Up & Down' },
]
type Direction = NonNullable<
NonNullable<MagicToastAddOptions['drag']>['direction']
>
const resolvedDirection = computed<Direction>(() => {
switch (direction.value) {
case 'left-right':
return ['left', 'right']
case 'up-down':
return ['up', 'down']
case 'left':
case 'right':
case 'up':
case 'down':
return direction.value
default:
return 'auto'
}
})
function onClick() {
add({
component,
props: { message: draggable.value ? 'Drag me around' : 'Not draggable' },
options: {
duration: Number(duration.value),
drag: {
disabled: !draggable.value,
direction: resolvedDirection.value,
},
threshold: { distance: Number(distance.value) },
animation: { snap: { duration: Number(snapDuration.value) } },
},
})
}
</script>Drag Direction
Use drag.direction to pin the drag-out side ('left' | 'right' | 'up' | 'down') regardless of position, or pass a same-axis pair like ['left', 'right'] to allow dragging either way.
<template>
<div class="flex w-full gap-2">
<m-button @click="onClick">Add Toast</m-button>
<m-select
v-model="position"
:options="positionOptions"
label="Position"
placeholder="Position"
/>
<m-select
v-model="direction"
:options="directionOptions"
label="Drag direction"
placeholder="Drag direction"
/>
</div>
<magic-toast-provider
id="magic-toast-drag-direction-demo"
:options="{
position: selectedPosition,
drag: { direction: resolvedDirection },
layout: {
expand: false,
max: 10,
},
initial: {
expanded: true,
},
}"
/>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, ref } from 'vue'
import { MButton, MSelect } from 'mirror-compat'
import { useMagicToast } from '@maas/vue-equipment/plugins/MagicToast'
import type { MagicToastOptions } from '@maas/vue-equipment/plugins/MagicToast'
type ToastPosition = NonNullable<MagicToastOptions['position']>
type ToastDragDirection = NonNullable<
NonNullable<MagicToastOptions['drag']>['direction']
>
const component = defineAsyncComponent(
() => import('./components/DemoToast.vue')
)
const { add } = useMagicToast('magic-toast-drag-direction-demo')
const position = ref<string | number>('bottom-right')
const direction = ref<string | number>('left-right')
const positionOptions: Array<{ value: ToastPosition; label: string }> = [
{ value: 'top-right', label: 'Top Right' },
{ value: 'top', label: 'Top' },
{ value: 'top-left', label: 'Top Left' },
{ value: 'left', label: 'Left' },
{ value: 'bottom-left', label: 'Bottom Left' },
{ value: 'bottom', label: 'Bottom' },
{ value: 'bottom-right', label: 'Bottom Right' },
{ value: 'right', label: 'Right' },
]
// MSelect only supports a single value, so the same-axis pairs are
// offered as presets that carry their array for the option.
const directionOptions: Array<{
value: string
label: string
direction: ToastDragDirection
}> = [
{ value: 'auto', label: 'Auto', direction: 'auto' },
{ value: 'left', label: 'Left', direction: 'left' },
{ value: 'right', label: 'Right', direction: 'right' },
{ value: 'up', label: 'Up', direction: 'up' },
{ value: 'down', label: 'Down', direction: 'down' },
{ value: 'left-right', label: 'Left & Right', direction: ['left', 'right'] },
{ value: 'up-down', label: 'Up & Down', direction: ['up', 'down'] },
]
const selectedPosition = computed(
() => positionOptions.find((option) => option.value === position.value)?.value
)
const resolvedDirection = computed(
() =>
directionOptions.find((option) => option.value === direction.value)
?.direction
)
// Mirrors the fallback in useToastDrag: when direction is 'auto',
// the actual drag axis/side is derived from the anchor position.
const messageDirection = computed(() => {
if (direction.value === 'left-right') {
return 'left or right'
}
if (direction.value === 'up-down') {
return 'up or down'
}
if (direction.value !== 'auto') {
return direction.value
}
switch (position.value) {
case 'top':
case 'top-left':
case 'top-right':
return 'up'
case 'bottom':
case 'bottom-left':
case 'bottom-right':
return 'down'
case 'left':
return 'left'
case 'right':
return 'right'
default:
return direction.value
}
})
function onClick() {
add({
component,
props: { message: `Drag me ${messageDirection.value} to dismiss` },
})
}
</script>Opacity Fade
The toast does not fade on its own. Listen to the drag event, derive an opacity from the drag distance, and reset it on afterDrag.
<template>
<div class="flex w-full gap-2">
<m-button @click="onClick">Add Toast</m-button>
</div>
<magic-toast-provider
:id="id"
:options="{
position: 'bottom-right',
threshold: { distance: dismissThreshold },
layout: { expand: false, max: 1 },
initial: { expanded: true },
}"
/>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, onBeforeUnmount, reactive } from 'vue'
import { MButton } from 'mirror-compat'
import { useMagicToast } from '@maas/vue-equipment/plugins/MagicToast'
import { useMagicEmitter } from '@maas/vue-equipment/plugins/MagicEmitter'
import type { MagicEmitterEvents } from '@maas/vue-equipment/plugins/MagicEmitter'
const id = 'magic-toast-opacity-fade-demo'
const component = defineAsyncComponent(
() => import('./components/FadeDemoToast.vue')
)
const { add } = useMagicToast(id)
const emitter = useMagicEmitter()
// Distance that dismisses the toast; the fade spans four times that
const dismissThreshold = 60
const fadeDistance = dismissThreshold * 4
const props = reactive({ message: 'Drag me away to fade me out', opacity: 1 })
// drag is shared across plugins, so the payload is a union — narrow it to the
// x/y shape before use. Derive opacity from the drag distance and reset it once
// the drag ends, entirely through the emitter — the toast no longer fades on its own.
function onDrag(payload: MagicEmitterEvents['drag']) {
if (payload.id !== id || !('x' in payload)) {
return
}
const distance = Math.max(Math.abs(payload.x), Math.abs(payload.y))
props.opacity = 1 - Math.min(distance / fadeDistance, 1)
}
function onAfterDrag(payload: MagicEmitterEvents['afterDrag']) {
if (payload.id !== id || !('x' in payload)) {
return
}
props.opacity = 1
}
emitter.on('drag', onDrag)
emitter.on('afterDrag', onAfterDrag)
onBeforeUnmount(() => {
emitter.off('drag', onDrag)
emitter.off('afterDrag', onAfterDrag)
})
function onClick() {
add({ component, props })
}
</script>Clear All
Call clear() to remove every toast in the stack at once, optionally with a custom transition name.
<template>
<div class="flex gap-2">
<m-button @click="onClick">Add Toast</m-button>
<m-button @click="clear">Clear All</m-button>
</div>
<magic-toast-provider id="magic-toast-clear-all-demo" />
</template>
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue'
import { MButton } from 'mirror-compat'
import { useMagicToast } from '@maas/vue-equipment/plugins/MagicToast'
const count = ref(0)
const component = defineAsyncComponent(
() => import('./components/DemoToast.vue')
)
const { add, clear } = useMagicToast('magic-toast-clear-all-demo')
function onClick() {
count.value += 1
const props = {
message: count.value % 2 ? 'Magic as a Service™' : 'Vue Equipment',
}
add({ component, props })
}
</script>Hide and Show
Call hide() to temporarily remove the whole stack from view, and show() to restore it exactly as it was — props, timers and all.
<template>
<div class="flex gap-2">
<m-button @click="onClick">Add Toast</m-button>
<m-button :disabled="toastCount === 0" @click="hide">Hide</m-button>
<m-button :disabled="hidden === 0" @click="show">Show</m-button>
</div>
<magic-toast-provider
id="magic-toast-hide-show-demo"
:options="{
layout: {
expand: false,
max: 10,
},
initial: {
expanded: true,
},
}"
/>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue'
import { MButton } from 'mirror-compat'
import { useMagicToast } from '@maas/vue-equipment/plugins/MagicToast'
const count = ref(0)
const component = defineAsyncComponent(
() => import('./components/DemoToast.vue')
)
const {
add,
hide,
show,
count: toastCount,
hidden,
} = useMagicToast('magic-toast-hide-show-demo')
function onClick() {
count.value += 1
const props = {
message: count.value % 2 ? 'Magic as a Service™' : 'Vue Equipment',
}
add({ component, props })
}
</script>Hide and Show with a Custom Transition
Pass a transition name to hide()/show() to swap in a different animation just for that call, without changing the toaster’s configured transition option.
<template>
<div class="flex gap-2">
<m-button @click="onClick">Add Toast</m-button>
<m-button :disabled="toastCount === 0" @click="hide('zoom')">
Hide (zoom)
</m-button>
<m-button :disabled="hidden === 0" @click="show('zoom')">
Show (zoom)
</m-button>
</div>
<magic-toast-provider
id="magic-toast-hide-show-transition-demo"
:options="{
layout: {
expand: false,
max: 10,
},
initial: {
expanded: true,
},
}"
/>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue'
import { MButton } from 'mirror-compat'
import { useMagicToast } from '@maas/vue-equipment/plugins/MagicToast'
import '@maas/vue-equipment/utils/css/keyframes/zoom-in.css'
import '@maas/vue-equipment/utils/css/keyframes/zoom-out.css'
const count = ref(0)
const component = defineAsyncComponent(
() => import('./components/DemoToast.vue')
)
const {
add,
hide,
show,
count: toastCount,
hidden,
} = useMagicToast('magic-toast-hide-show-transition-demo')
function onClick() {
count.value += 1
const props = {
message: count.value % 2 ? 'Magic as a Service™' : 'Vue Equipment',
}
add({ component, props })
}
</script>
<style>
/* Custom transition used only for hide('zoom') / show('zoom'),
passed as the transition name override on those calls. */
.zoom-enter-active {
animation: zoom-in var(--magic-toast-animation-duration) var(--ease-in-out);
}
.zoom-leave-active {
animation: zoom-out var(--magic-toast-animation-duration) var(--ease-in-out);
position: absolute;
}
.zoom-move {
transition: all var(--magic-toast-animation-duration) var(--ease-in-out);
}
</style>Slot
Pass slots to add() to render arbitrary content inside the toast component via its default slot.
<template>
<m-button @click="onClick">Add Toast</m-button>
<magic-toast-provider id="magic-toast-slot-demo" />
</template>
<script lang="ts" setup>
import { defineAsyncComponent, h, ref } from 'vue'
import { MButton } from 'mirror-compat'
import { useMagicToast } from '@maas/vue-equipment/plugins/MagicToast'
import type { Slots } from 'vue'
const count = ref(0)
const component = defineAsyncComponent(
() => import('./components/DemoToast.vue')
)
const { add } = useMagicToast('magic-toast-slot-demo')
function onClick() {
count.value += 1
// Break reactivity for count
const mappedCount = count.value
const slots: Slots = {
default: () => [h('span', `Message #${mappedCount}`)],
}
add({ component, slots })
}
</script>Slotted Component
Pass a component (instead of plain content) through slots, so the toast can render nested, composed markup.
<template>
<m-button @click="onClick">Add Toast</m-button>
<magic-toast-provider id="magic-toast-nested-slot-demo" />
</template>
<script lang="ts" setup>
import { defineAsyncComponent, h, ref } from 'vue'
import { MButton } from 'mirror-compat'
import { useMagicToast } from '@maas/vue-equipment/plugins/MagicToast'
import type { Slots } from 'vue'
const count = ref(0)
const component = defineAsyncComponent(
() => import('./components/DemoToast.vue')
)
const slot = defineAsyncComponent(() => import('./components/DemoSlot.vue'))
const { add } = useMagicToast('magic-toast-nested-slot-demo')
function onClick() {
count.value += 1
// Break reactivity for count
const mappedCount = count.value
const slots: Slots = {
default: () => [h(slot, { message: `Component #${mappedCount}` })],
}
add({ component, slots })
}
</script>