Notifications and dialogs
Notifications appear in one place: a toaster at the top of the viewport. Dialogs interrupt the page with a modal. The two are built to work together, so a toast raised while a modal is open is still seen and announced.
Pick the right component
| You want to… | Use |
|---|---|
| Report the result of an action: saved, sent, failed | A toast, through I9kToaster |
| Show standing status in the page, such as "This course is archived" | I9kToast, in the page flow |
| Interrupt for a focused task, such as an edit form | I9kModal |
| Explain a control on hover or focus | I9kTooltip |
| Announce something site-wide above the navigation | I9kAnnouncementBar |
Never show the same message both ways. A toast replaces an inline banner for an action's result.
Set up the toaster
Install one store per app and render one toaster at the root, before the page content in the tree. A toaster never announces a toast raised before its own setup ran, so one rendered below the page would show, but not announce, a toast the page raised while setting up.
// main.ts
import { createI9kToaster } from '@9klabs/design';
app.use(createI9kToaster({ labels: { region: 'Notifications', dismiss: 'Dismiss' } }));<!-- App.vue -->
<template>
<I9kToaster />
<RouterView />
</template>Translate through the store, not through props. Its labels object is reactive; assign its fields when the locale changes:
const toaster = useI9kToaster();
Object.assign(toaster.labels, { region: t('notifications.region'), dismiss: t('dismiss') });Raise a toast
From state you already have. useI9kToastSource follows a getter: the toast shows while it returns options, updates when they change, and goes away when it returns null or the component unmounts.
const error = ref<string | null>(null);
useI9kToastSource(() =>
error.value ? { id: 'login', variant: 'error', message: error.value } : null,
);
async function signIn() {
error.value = null; // clear first, so the same failure twice shows twice
try {
await login();
} catch (failure) {
error.value = 'Those details did not match.';
}
}Imperatively. useI9kToaster() returns the store:
const toaster = useI9kToaster();
const id = toaster.show({ variant: 'success', message: 'Changes saved.' });
toaster.dismiss(id);A toast is plain text. info and success dismiss themselves after five seconds. warning and error stay until dismissed, since a reader may need time to act on them. Showing a toast with an id already on screen replaces it rather than stacking a duplicate.
Open a modal
The parent owns open. The close button and Escape only ask to close, by emitting update:open(false). The modal closes when you set open to false, and focus returns to whatever opened it.
<script setup lang="ts">
import { ref } from 'vue';
import { I9kButton, I9kInput, I9kModal } from '@9klabs/design';
const open = ref(false);
const title = ref('');
const saving = ref(false);
</script>
<template>
<I9kButton @click="open = true">Edit module</I9kButton>
<I9kModal
v-model:open="open"
title="Edit module"
description="Both languages are required."
:dismissible="!saving"
>
<form id="edit-module" @submit.prevent="save">
<I9kInput v-model="title" label="Title" autofocus />
</form>
<template #footer>
<I9kButton @click="open = false">Cancel</I9kButton>
<I9kButton variant="primary" type="submit" form="edit-module">Save</I9kButton>
</template>
</I9kModal>
</template>Three behaviors to rely on
- Clicking the backdrop never closes a modal, so a stray click cannot lose an unsaved form.
- The body and footer render only while open, so a form inside starts fresh every time. Put
autofocuson its first field. - Set
dismissibletofalsewhile a save is in flight, so it cannot be closed halfway.
Toasts and modals together
An open I9kModal renders a toaster of its own inside the dialog, one layer above the page's. Only the deepest one shows toasts, so a toast raised while the modal is open is readable, announced and dismissable above the inert page. I9kSidebarLayout's drawer does the same on narrow screens. There is nothing to wire up.
Raise the result of a modal's task in the same tick you close it, in either order:
async function save() {
saving.value = true;
await updateModule(title.value);
saving.value = false;
open.value = false;
toaster.show({ variant: 'success', message: 'Module saved.' });
}The page's toaster shows that toast and announces it once the dialog has closed. A toast the modal already announced is never announced again by the page.
Keep modals out of the green section
An I9kModal placed inside an I9kSection variant="primary" inherits its re-mapped colors and turns translucent. Render the modal outside the section.