Keyboard shortcut: Ctrl + K
Get started

Toast

Notifications raised from anywhere in the code by calling toast(), and shown by a single VToaster mounted once. Several may stack, each with its own countdown.

Usage

vue
<script setup lang="ts">
import { VButton, toast } from 'vectis-ui'
</script>

<template>
  <!-- `toast()` can be called from anywhere: a component, a store, the handler that
       just saved something. What shows it is a single <VToaster /> mounted once at the
       root of the application, and only one, since the queue behind these calls has a
       single renderer. -->
  <VButton
    variant="outline"
    tone="neutral"
    @click="toast({ tone: 'success', message: 'Changes saved.' })"
  >
    Save
  </VButton>
</template>

Examples

Variants and tones

tone says what the notification means, with five values, and decides which icon it takes when none is given. variant is how strongly that tone is painted, tinted or solid.

vue

soft, the default

solid

<script setup lang="ts">
import { VButton, toast, type ToastTone } from 'vectis-ui'

const tones: ToastTone[] = ['neutral', 'accent', 'success', 'warning', 'danger']
</script>

<template>
  <!-- Raised into the <VToaster /> mounted once at the root of the application. -->
  <div class="demo">
    <!-- The tone says what the notification means, and it also decides which icon it
         takes when none is given. Five of them, because a notification reports a state
         and success and warning are states. -->
    <div class="row">
      <p class="caption">soft, the default</p>
      <div class="buttons">
        <VButton
          v-for="tone in tones"
          :key="tone"
          variant="outline"
          tone="neutral"
          @click="toast({ tone, message: `A ${tone} notification.` })"
        >
          {{ tone }}
        </VButton>
      </div>
    </div>

    <!-- The variant is how strongly it is painted: a tinted background with a border,
         or the full colour. -->
    <div class="row">
      <p class="caption">solid</p>
      <div class="buttons">
        <VButton
          v-for="tone in tones"
          :key="tone"
          variant="outline"
          tone="neutral"
          @click="toast({ tone, variant: 'solid', message: `A solid ${tone} notification.` })"
        >
          {{ tone }}
        </VButton>
      </div>
    </div>
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-5);
}
.row {
  display: grid;
  gap: var(--vectis-space-2);
}
.buttons {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-3);
}
.caption {
  margin: 0;
  color: var(--vectis-color-text-muted);
  font-size: var(--vectis-text-caption-size);
}
</style>

Title and message

message carries the notification, and title frames it in a few words when the message alone would not say what it is about.

vue
<script setup lang="ts">
import { VButton, toast } from 'vectis-ui'
</script>

<template>
  <!-- Raised into the <VToaster /> mounted once at the root of the application. -->
  <div class="demo">
    <!-- The message is the whole of it, and one sentence is usually enough. -->
    <VButton
      variant="outline"
      tone="neutral"
      @click="toast({ tone: 'success', message: 'Changes saved.' })"
    >
      Message only
    </VButton>

    <!-- A title frames the message when the message alone would not say what it is
         about. Keep it to a few words: it is a heading, not a first sentence. -->
    <VButton
      variant="outline"
      tone="neutral"
      @click="
        toast({
          tone: 'danger',
          title: 'Upload failed',
          message: 'invoice-2481.pdf is larger than the 10 MB limit.',
        })
      "
    >
      Title and message
    </VButton>
  </div>
</template>

<style scoped>
.demo {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-3);
}
</style>

Icons

Left out, icon comes from the tone. Naming one replaces it, and passing false removes it altogether.

vue
<script setup lang="ts">
import { VButton, toast } from 'vectis-ui'
import { cloud_upload as cloudUpload } from 'vectis-ui/icons'
</script>

<template>
  <!-- Raised into the <VToaster /> mounted once at the root of the application. -->
  <div class="demo">
    <!-- Left out, the icon comes from the tone: a notification arrives unannounced, so
         it is scanned before it is read and the glyph is what carries the meaning at a
         glance. -->
    <VButton
      variant="outline"
      tone="neutral"
      @click="toast({ tone: 'warning', message: 'Your session expires in 5 minutes.' })"
    >
      From the tone
    </VButton>

    <!-- Name one and it replaces the tone's, for a notification whose subject is more
         specific than its meaning. -->
    <VButton
      variant="outline"
      tone="neutral"
      @click="toast({ tone: 'success', icon: cloudUpload, message: 'Backup finished.' })"
    >
      A custom icon
    </VButton>

    <!-- `false` removes it altogether, which is not the same as leaving it out: the
         card then carries no glyph at all and the text runs to the edge. -->
    <VButton
      variant="outline"
      tone="neutral"
      @click="toast({ tone: 'accent', icon: false, message: 'Three new comments.' })"
    >
      No icon
    </VButton>
  </div>
</template>

<style scoped>
.demo {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-3);
}
</style>

Width

width takes any CSS length and replaces the card's own floor and ceiling. It is never allowed past the width of the viewport.

vue
<script setup lang="ts">
import { VButton, toast } from 'vectis-ui'

const LONG =
  'The export finished with 3 warnings. Rows 12, 48 and 91 were skipped because their reference column was empty.'
</script>

<template>
  <!-- Raised into the <VToaster /> mounted once at the root of the application. -->
  <div class="demo">
    <!-- Left alone the card sits between a floor and a ceiling of its own, so a short
         message is not a sliver and a long one does not stretch across the page. -->
    <VButton
      variant="outline"
      tone="neutral"
      @click="toast({ tone: 'warning', title: 'Export finished', message: LONG })"
    >
      Default width
    </VButton>

    <!-- Any CSS length replaces both. It is never allowed past the width of the
         viewport, margins included, so a value too large for a phone is simply
         ignored there rather than pushing the card off the screen. -->
    <VButton
      variant="outline"
      tone="neutral"
      @click="toast({ tone: 'warning', title: 'Export finished', message: LONG, width: '32rem' })"
    >
      32rem
    </VButton>

    <VButton
      variant="outline"
      tone="neutral"
      @click="toast({ tone: 'accent', message: 'Saved.', width: '14rem' })"
    >
      14rem
    </VButton>
  </div>
</template>

<style scoped>
.demo {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-3);
}
</style>

Placements

placement puts the notification in one of six corners, each with a stack of its own. Set on the VToaster it is the default for every notification; passed when one is raised it is that one's alone.

vue
<script setup lang="ts">
import { VButton, toast, type ToastPlacement } from 'vectis-ui'

const placements: ToastPlacement[] = [
  'top-left',
  'top-center',
  'top-right',
  'bottom-left',
  'bottom-center',
  'bottom-right',
]
</script>

<template>
  <!-- Raised into the <VToaster /> mounted once at the root of the application. -->
  <div class="demo">
    <!-- Six corners, each with a stack of its own, so notifications aimed at different
         ones never queue behind each other. Set on the VToaster the placement is the
         default for every notification; passed here it is this one's alone. -->
    <VButton
      v-for="placement in placements"
      :key="placement"
      variant="outline"
      tone="neutral"
      @click="toast({ placement, message: `Raised at ${placement}.` })"
    >
      {{ placement }}
    </VButton>
  </div>
</template>

<style scoped>
.demo {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-3);
}
</style>

Stacking

Notifications stack rather than replace one another, each keeping a countdown of its own, so they go away as their own clocks run out.

vue
<script setup lang="ts">
import { VButton, toast } from 'vectis-ui'

let n = 0

function raise() {
  n += 1
  toast({ tone: 'accent', message: `Notification ${n}.` })
}

function raiseThree() {
  toast({ tone: 'success', message: 'Backup finished.' })
  toast({ tone: 'warning', message: 'Two files were skipped.' })
  toast({ tone: 'danger', message: 'The report could not be sent.' })
}
</script>

<template>
  <!-- Raised into the <VToaster /> mounted once at the root of the application. -->
  <div class="demo">
    <!-- Notifications stack rather than replace one another, because two states can be
         true at the same time: a finished backup and a failed send are both worth
         reading. Each keeps a countdown of its own, so they go away in the order their
         own clocks run out and not in the order they arrived. -->
    <VButton variant="outline" tone="neutral" @click="raise">Raise one more</VButton>
    <VButton variant="outline" tone="neutral" @click="raiseThree">Raise three at once</VButton>
  </div>
</template>

<style scoped>
.demo {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-3);
}
</style>

How long it stays

duration is how long a notification stays, five seconds by default, and every one may ask for its own. The countdown holds while the pointer rests anywhere on the stack and while the keyboard is inside it, and is released only when both are gone.

vue
<script setup lang="ts">
import { VButton, toast } from 'vectis-ui'
</script>

<template>
  <!-- Raised into the <VToaster /> mounted once at the root of the application. -->
  <div class="demo">
    <!-- Five seconds by default, and every notification may ask for its own. The
         countdown holds while the pointer rests anywhere on the stack, so something
         that disappears on a clock can be read to the end. -->
    <VButton
      variant="outline"
      tone="neutral"
      @click="toast({ tone: 'accent', message: 'Gone in 2 seconds.', duration: 2000 })"
    >
      2 seconds
    </VButton>

    <VButton
      variant="outline"
      tone="neutral"
      @click="
        toast({
          tone: 'warning',
          title: 'Take your time',
          message: 'Gone in 10 seconds, unless the pointer rests on it.',
          duration: 10000,
        })
      "
    >
      10 seconds
    </VButton>
  </div>
</template>

<style scoped>
.demo {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-3);
}
</style>

Persistent notifications

A duration of 0 disarms the countdown and the notification stays until it is dismissed. Leave the close cross on so there is a way out.

vue
<script setup lang="ts">
import { VButton, toast } from 'vectis-ui'
</script>

<template>
  <!-- Raised into the <VToaster /> mounted once at the root of the application. -->
  <!-- A duration of 0 disarms the countdown: the notification then stays until it is
       dismissed. Reserve it for something the reader has to see, a failure they can
       act on, and leave the close cross on so there is a way out. -->
  <VButton
    variant="outline"
    tone="neutral"
    @click="
      toast({
        tone: 'danger',
        title: 'Connection lost',
        message: 'Your changes are kept locally until the connection comes back.',
        duration: 0,
      })
    "
  >
    Stays until dismissed
  </VButton>
</template>

Dismissing

hideClose takes the close cross away. toast hands back an id and dismissToast takes that notification away, or every one at once when called with no argument.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VButton, dismissToast, toast } from 'vectis-ui'

const id = ref<number | null>(null)

function raise() {
  // `toast` hands back an id, which is what lets this notification be taken away from
  // code: the request it was about came back, or the state it reported is over.
  id.value = toast({ tone: 'accent', message: 'Uploading…', duration: 0 })
}

function dismissOne() {
  if (id.value !== null) dismissToast(id.value)
  id.value = null
}
</script>

<template>
  <!-- Raised into the <VToaster /> mounted once at the root of the application. -->
  <div class="demo">
    <!-- The close cross is on by default, and turning it off only makes sense on a
         notification that goes away on its own: with no countdown and no cross,
         nothing but code can remove it. -->
    <VButton
      variant="outline"
      tone="neutral"
      @click="toast({ tone: 'success', message: 'Saved.', hideClose: true })"
    >
      No cross
    </VButton>

    <VButton variant="outline" tone="neutral" @click="raise">Raise one to dismiss</VButton>
    <VButton variant="outline" tone="neutral" @click="dismissOne">Dismiss that one</VButton>

    <!-- With no argument it clears every notification at once. -->
    <VButton variant="outline" tone="neutral" @click="dismissToast()">Dismiss all</VButton>
  </div>
</template>

<style scoped>
.demo {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-3);
}
</style>

API

Props

PropTypeDefault
placementToastPlacement'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right''bottom-right'
Which corner notifications appear in, unless one of them asks for another.
durationnumber5000
How long a notification stays, in milliseconds, unless it asks for something else. A notification given 0 stays until it is dismissed.
closeLabelstringnone
What the close cross does, in words. It falls back to the design system dictionary.
labelstringnone
What screen readers announce for the notification areas themselves, which are landmarks of the page. It falls back to the design system dictionary.

CSS variables

TokenValue
--vectis-control-size-toast-width22rem