Setup
For the beginning, add the Toaster
component to your app.vue
.
app.vue
<template>
<div>
<NuxtPage />
<NToaster />
</div>
</template>
Examples
Basic
Then, you can use the useToast
composable to add toasts to your app:
Prop | Default | Type | Description |
---|---|---|---|
title | - | string | Title of the toast |
description | - | string | Description of the toast |
showProgress | false | boolean | Show the progress bar. |
closable | true | boolean | Display close button. |
Preview
Code
<script setup lang="ts">
const { toast } = useToast()
</script>
<template>
<!-- Optional -->
<NToaster />
<div class="grid h-28 place-items-center">
<NButton
label="Show toast"
btn="solid-white"
@click="toast(
{
title: 'Example Toast',
duration: 5 * 1000,
showProgress: true,
description: 'This toast automatically closes.',
},
)"
/>
</div>
</template>
With actions
Prop | Default | Type | Description |
---|---|---|---|
actions | [] | Action[] | The array of action. |
Preview
Code
<script setup lang="ts">
const { toast } = useToast()
const actions = [
{
label: 'Retry',
btn: 'solid-primary',
altText: 'Error',
onClick: () => {
alert('Retry clicked')
},
},
{
label: 'Dismiss',
btn: 'solid-white',
altText: 'Error',
onClick: () => {
alert('Dismiss clicked')
},
},
]
</script>
<template>
<div class="grid h-28 place-items-center">
<NButton
label="Show toast"
btn="solid-white"
@click="toast(
{
title: 'Uh oh! Something went wrong.',
description: 'There was a problem with your request.',
actions,
},
)"
/>
</div>
</template>
Leading Icon
Prop | Default | Type | Description |
---|---|---|---|
leading | - | string | The leading icon of the toast |
Preview
Code
<script setup lang="ts">
const { toast } = useToast()
</script>
<template>
<div class="grid h-28 place-items-center">
<NButton
label="Show toast"
btn="solid-white"
@click="toast(
{
title: 'Success Toast',
leading: 'i-check',
description: 'This toast is a success message.',
},
)"
/>
</div>
</template>
Read more in Icon Component
Variant and Color
Prop | Default | Type | Description |
---|---|---|---|
toast | outline-gray | {variant}-{color} | Set the toast variant and color. |
progress | primary | {color} | Set the progress color. |
NToastAction
is wrapped around the NButton component. This means that all the props and slots of
NButton
are available to use or through toast-action
prop.
Preview
Code
<script setup lang="ts">
const { toast } = useToast()
const actions = ref([
{
label: 'Try again',
click: () => alert('Try again'),
altText: 'Try again',
},
])
</script>
<template>
<div class="h-28 flex flex-wrap items-center justify-around gap-4">
<NButton
label="Base toast"
btn="solid-white"
@click="toast(
{
title: 'Uh oh! Something went wrong.',
description: 'There was a problem with your request.',
toast: 'solid-white',
progress: 'white',
showProgress: true,
actions,
closable: true,
},
)"
/>
<NButton
label="soft-success toast"
btn="soft-success"
@click="toast(
{
title: 'Success! Your request was processed.',
description: 'Your request has been successfully processed.',
toast: 'soft-success',
progress: 'success',
showProgress: true,
closable: true,
},
)"
/>
<NButton
label="soft-warning toast"
btn="soft-warning"
@click="toast(
{
title: 'Uh oh! Something went wrong.',
description: 'There was a problem with your request.',
toast: 'soft-warning',
progress: 'warning',
showProgress: true,
actions,
closable: true,
},
)"
/>
</div>
</template>
Provider
Configure the toast provider using the _toastProvider
prop.
Prop | Default | Type | Description |
---|---|---|---|
duration | 4000 | number | Set the duration in milliseconds of the toast. |
label | Notification | string | An author-localized label for each toast. |
swipeDirection | right | right left up down | Direction of pointer swipe that should close the toast. |
swipeThreshold | 50 | number | Distance in pixels that the swipe pass before a close is triggered. |
Read more in Radix Toast Root API.
Preview
Code
<script setup lang="ts">
const { toast } = useToast()
</script>
<template>
<div class="grid h-28 place-items-center">
<NButton
label="Show toast"
btn="solid-white"
@click="toast(
{
title: 'Uh oh! Something went wrong.',
description: 'There was a problem with your request.',
_toastProvider: {
duration: 1000,
swipeDirection: 'up',
},
closable: true,
},
)"
/>
</div>
</template>
Slots
Name | Props | Description |
---|---|---|
default | - | The trigger slot. |
actions | - | The actions slot. |
info | - | The info slot. |
title | - | The title slot. |
description | - | The description slot. |
closeIcon | - | The close icon slot. |
Presets
shortcuts/toast.ts
type ToastPrefix = 'toast'
export const staticToast: Record<`${ToastPrefix}-${string}` | ToastPrefix, string> = {
// config
'toast': 'pointer-events-auto relative flex w-full space-x-2 justify-end overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all ',
'toast-viewport': 'fixed top-0 z-100 flex max-h-screen gap-y-4 w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-420px',
'toast-wrapper': 'w-0 flex flex-1 flex-col gap-2',
'toast-title': 'text-sm font-semibold [&+div]:text-xs',
'toast-description': 'text-sm opacity-90',
'toast-leading': 'square-5',
'toast-close': 'bg-transparent flex items-center justify-center absolute right-1 top-1 rounded-md p-1 text-brand/80 opacity-0 transition-opacity hover:text-brand focus:opacity-100 focus:ring-1 focus:ring-brand/80 focus:outline-none group-hover:opacity-100',
'toast-close-icon': 'i-close',
'toast-close-icon-base': 'h-1em w-1em',
'toast-info': 'grid gap-1',
'toast-actions': 'flex shrink-0 gap-1.5',
'toast-progress': 'h-1 rounded-none',
}
export const dynamicToast = [
// dynamic variants
[/^toast-solid(-(\S+))?$/, ([, , c = 'primary']) => `alert-solid-${c}`],
[/^toast-soft(-(\S+))?$/, ([, , c = 'primary']) => `alert-soft-${c}`],
[/^toast-outline(-(\S+))?$/, ([, , c = 'primary']) => `alert-outline-${c}`],
[/^toast-border(-(\S+))?$/, ([, , c = 'primary']) => `alert-border-${c}`],
]
export const toast = [
...dynamicToast,
staticToast,
]
Props
types/toast.ts
import type { ToastActionProps, ToastCloseProps, ToastDescriptionProps, ToastProviderProps, ToastRootProps, ToastTitleProps, ToastViewportProps } from 'radix-vue'
import type { HTMLAttributes } from 'vue'
import type { NButtonProps } from './button'
import type { NProgressProps } from './progress'
interface BaseExtensions {
class?: HTMLAttributes['class']
}
export type Toaster = NToastProps & { id: string }
export interface NToasterProps extends BaseExtensions, ToastProviderProps {
_toastViewport?: Partial<NToastViewportProps>
_toast?: Partial<NToastProps>
}
export interface NToastProps extends BaseExtensions, Pick<NProgressProps, 'progress'>, ToastRootProps {
/**
* Allows you to add `UnaUI` toast preset properties,
* Think of it as a shortcut for adding options or variants to the preset if available.
*
* @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/toast.ts
* @example
* toast="outline-green"
*/
toast?: HTMLAttributes['class']
/**
* Display leading icon.
*
* @default null
*/
leading?: HTMLAttributes['class']
/**
* Add a title to the toast.
*/
title?: string
/**
* Add a description to the toast.
*/
description?: string
/**
* Display `close` icon on the right side of the toast.
*
* @default false
*/
closable?: boolean
/**
* The array of actions.
*/
actions?: ToastActionProps[]
/**
* The array of toasts
*/
toasts?: Toaster[]
/**
* Allows you to change the size of the input.
*
* @default sm
*
* @example
* size="sm" | size="2cm" | size="2rem" | size="2px"
*/
size?: string
/**
* Show progress bar on the toast.
*
* @default false
*/
showProgress?: boolean
// Subcomponents
_toastProvider?: Partial<NToastProviderProps>
_toastTitle?: Partial<NToastTitleProps>
_toastDescription?: Partial<NToastDescriptionProps>
_toastViewport?: Partial<NToastViewportProps>
_toastAction?: Partial<NToastActionProps>
_toastClose?: Partial<NToastCloseProps>
_toastInfo?: Partial<NToastInfoProps>
_progress?: Partial<NProgressProps>
/**
* `UnaUI` preset configuration
*
* @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/toast.ts
*/
una?: NToastUnaProps
onOpenChange?: ((value: boolean) => void) | undefined
}
export interface NToastProviderProps extends ToastProviderProps {}
export interface NToastTitleProps extends ToastTitleProps, BaseExtensions {
/** Additional properties for the una component */
una?: Pick<NToastUnaProps, 'toastTitle'>
}
export interface NToastDescriptionProps extends ToastDescriptionProps, BaseExtensions {
/** Additional properties for the una component */
una?: Pick<NToastUnaProps, 'toastDescription'>
}
export interface NToastViewportProps extends ToastViewportProps, BaseExtensions {
/** Additional properties for the una component */
una?: Pick<NToastUnaProps, 'toastViewport'>
}
export interface NToastActionProps extends NButtonProps, ToastActionProps {
}
export interface NToastInfoProps extends BaseExtensions {
/** Additional properties for the una component */
una?: Pick<NToastUnaProps, 'toastInfo'>
}
export interface NToastCloseProps extends ToastCloseProps, BaseExtensions {
/** Additional properties for the una component */
una?: Pick<NToastUnaProps, 'toastClose' | 'toastCloseIconBase' | 'toastCloseIcon'>
}
export interface NToastUnaProps {
toast?: HTMLAttributes['class']
toastRoot?: HTMLAttributes['class']
toastTitle?: HTMLAttributes['class']
toastDescription?: HTMLAttributes['class']
toastViewport?: HTMLAttributes['class']
toastClose?: HTMLAttributes['class']
toastCloseIconBase?: HTMLAttributes['class']
toastCloseIcon?: HTMLAttributes['class']
toastInfo?: HTMLAttributes['class']
toastLeading?: HTMLAttributes['class']
toastWrapper?: HTMLAttributes['class']
toastProgress?: HTMLAttributes['class']
}
Composables
useToast.ts
import type { ComputedRef, VNode } from 'vue'
import type { NToastProps } from '../types'
import { computed, ref } from 'vue'
const TOAST_LIMIT = 10
const TOAST_REMOVE_DELAY = 1000000
export type StringOrVNode =
| string
| VNode
| (() => VNode)
type ToasterToast = NToastProps & {
id: string
title?: string
description?: StringOrVNode
actions?: NToastProps[]
}
const actionTypes = {
ADD_TOAST: 'ADD_TOAST',
UPDATE_TOAST: 'UPDATE_TOAST',
DISMISS_TOAST: 'DISMISS_TOAST',
REMOVE_TOAST: 'REMOVE_TOAST',
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_VALUE
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType['ADD_TOAST']
toast: ToasterToast
}
| {
type: ActionType['UPDATE_TOAST']
toast: Partial<ToasterToast>
}
| {
type: ActionType['DISMISS_TOAST']
toastId?: ToasterToast['id']
}
| {
type: ActionType['REMOVE_TOAST']
toastId?: ToasterToast['id']
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
function addToRemoveQueue(toastId: string) {
if (toastTimeouts.has(toastId))
return
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: actionTypes.REMOVE_TOAST,
toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
const state = ref<State>({
toasts: [],
})
function dispatch(action: Action) {
switch (action.type) {
case actionTypes.ADD_TOAST:
state.value.toasts = [action.toast, ...state.value.toasts].slice(0, TOAST_LIMIT)
break
case actionTypes.UPDATE_TOAST:
state.value.toasts = state.value.toasts.map(t =>
t.id === action.toast.id ? { ...t, ...action.toast } : t,
)
break
case actionTypes.DISMISS_TOAST: {
const { toastId } = action
if (toastId) {
addToRemoveQueue(toastId)
}
else {
state.value.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
state.value.toasts = state.value.toasts.map(t =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t,
)
break
}
case actionTypes.REMOVE_TOAST:
if (action.toastId === undefined)
state.value.toasts = []
else
state.value.toasts = state.value.toasts.filter(t => t.id !== action.toastId)
break
}
}
type Toast = Omit<ToasterToast, 'id'>
function toast(props: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: actionTypes.UPDATE_TOAST,
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: actionTypes.DISMISS_TOAST, toastId: id })
dispatch({
type: actionTypes.ADD_TOAST,
toast: {
...props,
id,
open: true,
onOpenChange: (open: boolean) => {
if (!open)
dismiss()
},
},
})
return {
id,
dismiss,
update,
}
}
interface UseToast {
toasts: ComputedRef<ToasterToast[]>
toast: (props: Toast) => { id: string, dismiss: () => void, update: (props: ToasterToast) => void }
dismiss: (toastId?: string) => void
}
function useToast(): UseToast {
return {
toasts: computed(() => state.value.toasts),
toast,
dismiss: (toastId?: string) => dispatch({ type: actionTypes.DISMISS_TOAST, toastId }),
}
}
export { toast, useToast }
Components
Toast.vue
Toaster.vue
ToastRoot.vue
ToastProvider.vue
ToastViewport.vue
ToastInfo.vue
ToastTitle.vue
ToastDescription.vue
ToastAction.vue
ToastClose.vue
<script setup lang="ts">
import type { NToastProps } from '../../../types'
import { reactivePick } from '@vueuse/core'
import { ToastRoot, type ToastRootEmits, useForwardPropsEmits } from 'radix-vue'
import { cn } from '../../../utils'
import Icon from '../../elements/Icon.vue'
import Progress from '../../elements/Progress.vue'
import ToastAction from './ToastAction.vue'
import ToastClose from './ToastClose.vue'
import ToastDescription from './ToastDescription.vue'
import ToastInfo from './ToastInfo.vue'
import ToastTitle from './ToastTitle.vue'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<NToastProps>(), {
toast: 'solid-white',
closable: true,
})
const emits = defineEmits<ToastRootEmits>()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultOpen', 'open', 'duration', 'type'), emits)
</script>
<template>
<ToastRoot
v-slot="{ remaining, duration }"
:class="cn(
'group toast data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
props.class,
props.una?.toast,
)"
v-bind="rootProps"
:toast
@update:open="onOpenChange"
>
<slot name="leading">
<Icon
v-if="leading"
:name="leading"
:class="cn(
'toast-leading',
props.una?.toastLeading,
)"
/>
</slot>
<div
:class="cn(
'toast-wrapper',
props.una?.toastWrapper,
)"
>
<slot>
<ToastInfo
v-if="$slots.info || $slots.title || $slots.description || title || description"
v-bind="_toastInfo"
:una
>
<slot name="info">
<ToastTitle
v-if="$slots.title || title"
v-bind="_toastTitle"
:una
>
<slot name="title">
{{ title }}
</slot>
</ToastTitle>
<ToastDescription
v-if="$slots.description || description"
v-bind="_toastDescription"
:una
>
<slot name="description">
{{ description }}
</slot>
</ToastDescription>
</slot>
</ToastInfo>
<div
v-if="actions"
class="toast-actions"
>
<slot name="actions" :actions>
<ToastAction
v-for="(action, index) in actions"
:key="index"
v-bind="action"
/>
</slot>
</div>
<ToastClose
v-if="closable"
v-bind="_toastClose"
:una
>
<slot name="closeIcon" />
</ToastClose>
</slot>
</div>
<div
v-if="showProgress"
class="absolute inset-x-0 bottom-0 !mx-0"
>
<Progress
:progress
v-bind="_progress"
:class="cn(
'toast-progress bg-transparent',
props.una?.toastProgress,
)"
:model-value="remaining / duration * 100"
/>
</div>
</ToastRoot>
</template>
<script setup lang="ts">
import type { NToasterProps } from '../../types'
import { reactivePick } from '@vueuse/core'
import { useToast } from '../../composables/useToast'
import Toast from './toast/Toast.vue'
import ToastProvider from './toast/ToastProvider.vue'
import ToastViewport from './toast/ToastViewport.vue'
defineOptions({
inheritAttrs: false,
})
const props = defineProps<NToasterProps>()
const slots = defineSlots<any>()
const rootProps = reactivePick(props, ['duration', 'label', 'swipeDirection', 'swipeThreshold'])
const { toasts } = useToast()
</script>
<template>
<ToastProvider
v-bind="rootProps"
>
<slot>
<Toast
v-for="t in toasts"
:key="t.id"
v-bind="{ ..._toast, ...$attrs, ...t }"
>
<template v-for="(_, name) in slots" #[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
</Toast>
</slot>
<ToastViewport v-bind="_toastViewport" />
</ToastProvider>
</template>
🔴 ../packages/nuxt/src/runtime/components/overlays/toast/ToastRoot.vue | Snippet does not exist 🔴
<script setup lang="ts">
import type { NToastProviderProps } from '../../../types'
import { ToastProvider } from 'radix-vue'
const props = withDefaults(defineProps<NToastProviderProps>(), {
duration: 5000,
})
</script>
<template>
<ToastProvider v-bind="props">
<slot />
</ToastProvider>
</template>
<script setup lang="ts">
import type { NToastViewportProps } from '../../../types'
import { ToastViewport } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '../../../utils'
const props = defineProps<NToastViewportProps>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<ToastViewport
v-bind="delegatedProps"
:class="cn(
'toast-viewport',
props.class,
props.una?.toastViewport,
)"
/>
</template>
<script setup lang="ts">
import type { NToastInfoProps } from '../../../types'
import { cn } from '../../../utils'
const props = defineProps<NToastInfoProps>()
</script>
<template>
<div
:class="cn(
'toast-info',
props.class,
props.una?.toastInfo,
)"
>
<slot />
</div>
</template>
<script setup lang="ts">
import type { NToastTitleProps } from '../../../types'
import { ToastTitle } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '../../../utils'
const props = defineProps<NToastTitleProps>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<ToastTitle
v-bind="delegatedProps"
:class="cn(
'toast-title',
props.class,
props.una?.toastTitle,
)"
>
<slot />
</ToastTitle>
</template>
<script setup lang="ts">
import type { NToastDescriptionProps } from '../../../types'
import { ToastDescription } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '../../../utils'
const props = defineProps<NToastDescriptionProps>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<ToastDescription
v-bind="delegatedProps"
:class="cn(
'toast-description',
props.class,
props.una?.toastDescription,
)"
>
<slot />
</ToastDescription>
</template>
<script setup lang="ts">
import type { NToastActionProps } from '../../../types'
import { ToastAction } from 'radix-vue'
import { computed } from 'vue'
import { cn, omitProps, randomId } from '../../../utils'
import Button from '../../elements/Button.vue'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<NToastActionProps>(), {
btn: 'solid-white',
size: 'xs',
})
const slots = defineSlots<any>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<ToastAction
:alt-text="altText"
as-child
>
<Button
v-bind="omitProps({ ...$attrs, ...delegatedProps }, ['altText'])"
:id="randomId('toast-action')"
:class="cn('toast-action', props.class)"
>
<template v-for="(_, name) in slots" #[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
</Button>
</ToastAction>
</template>
<script setup lang="ts">
import type { NToastCloseProps } from '../../../types'
import { ToastClose } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '../../../utils'
import Icon from '../../elements/Icon.vue'
const props = defineProps<NToastCloseProps>()
const delegatedProps = computed(() => {
const { ...delegated } = props
return delegated
})
</script>
<template>
<ToastClose
v-bind="delegatedProps"
:class="cn(
'toast-close',
props.class,
props.una?.toastClose,
)"
>
<Icon
:name="una?.toastCloseIcon ?? 'toast-close-icon'"
:class="cn(
'toast-close-icon-base',
una?.toastCloseIconBase,
)"
aria-hidden="true"
/>
</ToastClose>
</template>