Toast

A short piece of feedback that floats in once an action finishes.

<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>
vue

Usage

import { Toaster, toast } from '@hina-ui/vue'
ts

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>
vue
<template>
  <AppShell>
    <NuxtPage />
    <Toaster />
  </AppShell>
</template>
vue

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>
vue

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>
vue

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>
vue

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>
vue

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>
vue

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>
vue

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>
vue

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, which label overrides.
  • 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.