<script setup lang="ts">
import { Button, Inline, toast } from '@hina-ui/vue'
</script>
<template>
<Inline align="center" class="gap-3">
<Button variant="outline" tone="neutral" @click="toast('Draft saved')">Default</Button>
<Button variant="outline" tone="neutral" @click="toast.success('Article published')">
Success
</Button>
<Button
variant="outline"
tone="neutral"
@click="toast.danger('Upload failed', { description: 'The file is larger than 20 MB.' })"
>
Failure
</Button>
</Inline>
</template>
Usage
import { Toaster, toast } from '@hina-ui/vue'
Put one Toaster at the root of the app and call toast() from anywhere. It returns the id of that toast, which you can use to update or dismiss it.
<script setup lang="ts">
import { Button, toast } from '@hina-ui/vue'
</script>
<template>
<Button variant="outline" tone="neutral" @click="toast('Draft saved')">Save draft</Button>
</template>
<template>
<AppShell>
<NuxtPage />
<Toaster />
</AppShell>
</template>
Examples
Tones
Each of the five tones has its own icon and colour. loading shows a spinner and stays until dismissed, usually updated into a final result through its id.
<script setup lang="ts">
import { Button, Inline, toast } from '@hina-ui/vue'
</script>
<template>
<Inline align="center" class="gap-3">
<Button variant="outline" tone="neutral" @click="toast.success('Added to your shelf')">
success
</Button>
<Button variant="outline" tone="neutral" @click="toast.danger('The connection dropped')">
danger
</Button>
<Button variant="outline" tone="neutral" @click="toast.warning('You have unsaved changes')">
warning
</Button>
<Button variant="outline" tone="neutral" @click="toast.info('A new version is available')">
info
</Button>
<Button variant="outline" tone="neutral" @click="toast.loading('Syncing')">loading</Button>
</Inline>
</template>
Description
description is the second line under the message, for the details.
<script setup lang="ts">
import { Button, toast } from '@hina-ui/vue'
</script>
<template>
<Button
variant="outline"
tone="neutral"
@click="
toast.success('Import finished', {
description: '128 books imported, 3 skipped for an unsupported format.',
})
"
>
With a description
</Button>
</template>
Actions
action is the primary button and cancel the secondary one; either of them dismisses the toast when clicked. Set duration to 0 when the toast needs an answer.
<script setup lang="ts">
import { Button, Inline, toast } from '@hina-ui/vue'
</script>
<template>
<Inline align="center" class="gap-3">
<Button
variant="outline"
tone="neutral"
@click="
toast('Moved to the bin', {
action: { label: 'Undo', onClick: () => toast.success('Restored') },
})
"
>
With an action
</Button>
<Button
variant="outline"
tone="neutral"
@click="
toast.warning('Empty the bin?', {
duration: 0,
action: { label: 'Empty', onClick: () => toast.success('The bin is empty') },
cancel: { label: 'Cancel' },
})
"
>
Two buttons
</Button>
</Inline>
</template>
Async tasks
toast.promise takes a promise and moves through loading, success and error on its own. success and error can be functions, building the message from the result.
<script setup lang="ts">
import { Button, toast } from '@hina-ui/vue'
function upload() {
const task = new Promise<{ name: string }>(resolve =>
setTimeout(() => resolve({ name: 'beyond-the-stars.epub' }), 2000),
)
toast.promise(task, {
loading: 'Uploading',
success: book => `${book.name} uploaded`,
error: 'Upload failed, please try again',
})
}
</script>
<template>
<Button variant="outline" tone="neutral" @click="upload">Upload a file</Button>
</template>
Updating in place
Passing the same id updates the tone, the text and the timer of the existing toast instead of adding another one. Progress feedback works this way.
<script setup lang="ts">
import { Button, toast } from '@hina-ui/vue'
function sync() {
toast.loading('Syncing', { id: 'sync' })
setTimeout(() => toast.loading('Synced 12 of 30', { id: 'sync' }), 1000)
setTimeout(() => toast.success('Sync finished', { id: 'sync' }), 2000)
}
</script>
<template>
<Button variant="outline" tone="neutral" @click="sync">Start syncing</Button>
</template>
Custom rendering
toast.custom renders the body of a toast with your own component. Whatever is in props is passed straight through, and the component also receives a toastId. The surface, the border, the shadow and the padding still come from Toaster.
<script setup lang="ts">
import { defineComponent, h } from 'vue'
import { BookOpen } from '@lucide/vue'
import { Button, Heading, Inline, Stack, Text, toast } from '@hina-ui/vue'
const ChapterToast = defineComponent({
props: { title: { type: String, required: true }, chapter: { type: String, required: true } },
setup(props) {
return () =>
h(
Inline,
{ align: 'center', class: 'gap-3' },
{
default: () => [
h(
'div',
{ class: 'bg-inset grid size-10 shrink-0 place-items-center rounded-md' },
h(BookOpen, { class: 'size-5' }),
),
h(
Stack,
{ gap: 'none', class: 'min-w-0 flex-1' },
{
default: () => [
h(Heading, { level: 4, size: 'sm' }, () => props.title),
h(Text, { tone: 'muted', size: 'sm' }, () => props.chapter),
],
},
),
h(
Button,
{ size: 'sm', onClick: () => toast.dismiss('chapter') },
{ default: () => 'Read' },
),
],
},
)
},
})
function notify() {
toast.custom(ChapterToast, {
id: 'chapter',
duration: 6000,
props: { title: 'ATRI', chapter: 'Chapter 42' },
})
}
</script>
<template>
<Button variant="outline" tone="neutral" @click="notify">New chapter</Button>
</template>
How long it stays
duration is in milliseconds and defaults to 4000, or 0 for loading. A duration of 0 means it stays until the close button is pressed or toast.dismiss(id) is called.
<script setup lang="ts">
import { Button, Inline, toast } from '@hina-ui/vue'
</script>
<template>
<Inline align="center" class="gap-3">
<Button variant="outline" tone="neutral" @click="toast('Gone in a second', { duration: 1000 })">
One second
</Button>
<Button
variant="outline"
tone="neutral"
@click="toast.info('Stays until dismissed', { id: 'sticky', duration: 0 })"
>
No auto close
</Button>
<Button variant="soft" tone="neutral" @click="toast.dismiss('sticky')">Dismiss that one</Button>
</Inline>
</template>
Behaviour
- The timer pauses while the pointer hovers the toast area or focus is inside it, and resumes on leaving.
- At most five toasts are shown at once; older ones leave.
- Toasts are keyed by
id, so calling the same id again updates rather than adds. - Swiping a toast to the right dismisses it.
Accessibility
- The toast area is a
role="region"named “Notifications” by default, whichlabeloverrides. - Each toast announces through a separate
role="alert"live region, so a screen reader reads out the message and its description. - The topmost toast can be reached with Tab, and the close button carries an accessible name.
API
toast
| Method | Description |
|---|---|
toast(message, options?) | A neutral toast; returns its id |
toast.success(...) | Success |
toast.danger(...) | Failure |
toast.warning(...) | Warning |
toast.info(...) | Information |
toast.loading(...) | In progress; stays until dismissed |
toast.promise(p, messages) | Follows the three states of a promise |
toast.custom(component, {}) | Renders the whole toast with your own component |
toast.dismiss(id?) | Dismisses one toast, or all of them |
ToastOptions
| Prop | Type | Default | Description |
|---|---|---|---|
id | number | string | — | The same id updates the existing toast |
description | string | — | The second line under the message |
duration | number | 4000 | Milliseconds on screen; 0 never closes |
action | { label, onClick } | — | The primary button |
cancel | { label, onClick } | — | The secondary button |
onDismiss | (id) => void | — | Called when the toast is dismissed |
onAutoClose | (id) => void | — | Called when the timer closes it |
Toaster
| Prop | Type | Default | Description |
|---|---|---|---|
position | ToasterPosition | — | Bottom centre on a narrow screen, top right on a wide one |
label | string | — | Accessible name of the toast area |
class | string | — | Classes appended to the toast area |
ToasterPosition is one of top-start, top-center, top-end, bottom-start, bottom-center and bottom-end.