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>