Toast
A short notice that something happened. It shows in a corner, leaves by itself, and always has an x, because a swipe is a drag.
Toast
client - size not measured yetA short notice that something happened. It shows in a corner, leaves by itself, and always has an x, because a swipe is a drag.
Try it
The code
'use client'
import { Button } from 'topclassui/button'
import { toast } from 'topclassui/toast'
import type { ToastDesignProps } from 'topclassui/toast'
/** The docs stage: the Try it controls set the options of the toast the press adds. */
export default function Demo(props: ToastDesignProps = {}) {
return (
<Button
variant="outline"
onClick={() => { toast.add({ ...props, title: 'Project created', description: 'It is ready for its first deploy.' }) }}
>
Show a toast
</Button>
)
}
TopClassProvider mounts the one viewport, so there is nothing to mount; toast.add works from any browser module, inside React or not.
Every state
11 statesImport from the package
import { Toast } from 'topclassui/toast'Install once:npm i topclassui@beta zod
Examples
toastis four plain functions:add,update,closeandpromise. The table lists the options oftoast.add, not props; it also takes a required title, a description and one action.durationis clamped to 0 to 600000 ms, and 0 stays until it is closed. Left out, the provider's applies: 5000 unlessTopClassProvidersets another.- The action does not close the toast: call
toast.close(id)in its handler, with the idaddreturned.toast.close()with no id closes them all. toast.promiseshows one toast: a spinner, then the success or the error icon and text. It returns the promise, rejection included, so catch it.- Placement, limit and duration are set once on the provider. The newest toast is the nearest the edge, and three show at once; the next appears when one closes.
'use client'
import { Button } from 'topclassui/button'
import { toast } from 'topclassui/toast'
/** The drawn sample: a title and a line under it. */
export default function Demo() {
return (
<Button
variant="outline"
onClick={() => { toast.add({ tone: 'success', title: 'Invitation sent', description: 'They get an email within the minute.' }) }}
>
Invite
</Button>
)
}
'use client'
import { Button } from 'topclassui/button'
import { toast } from 'topclassui/toast'
/** One press, one toast. The viewport is already there: TopClassProvider mounts it. */
export default function Demo() {
return (
<Button variant="outline" onClick={() => { toast.add({ title: 'Project created' }) }}>
Create project
</Button>
)
}
'use client'
import { Button } from 'topclassui/button'
import { toast } from 'topclassui/toast'
/** The six tones. Four carry an icon, so the meaning never rests on colour alone; neutral and primary carry none. */
export default function Demo() {
return (
<div className="flex flex-wrap gap-2">
<Button variant="outline" onClick={() => { toast.add({ title: 'Draft saved' }) }}>Neutral</Button>
<Button variant="outline" onClick={() => { toast.add({ tone: 'primary', title: 'New version available' }) }}>Primary</Button>
<Button variant="outline" onClick={() => { toast.add({ tone: 'success', title: 'Invitation sent' }) }}>Success</Button>
<Button variant="outline" onClick={() => { toast.add({ tone: 'warning', title: 'Storage almost full' }) }}>Warning</Button>
<Button variant="outline" onClick={() => { toast.add({ tone: 'danger', title: 'Could not save' }) }}>Danger</Button>
<Button variant="outline" onClick={() => { toast.add({ tone: 'info', title: 'Maintenance tonight' }) }}>Info</Button>
</div>
)
}
'use client'
import { Button } from 'topclassui/button'
import { toast } from 'topclassui/toast'
/** An action under the text. It does not close the toast by itself, so Undo closes it by the id add() returned. */
export default function Demo() {
const archive = () => {
const id = toast.add({
title: 'Project archived',
action: { label: 'Undo', onClick: () => { toast.close(id) } },
})
}
return <Button variant="outline" onClick={archive}>Archive</Button>
}
'use client'
import { Button } from 'topclassui/button'
import { toast } from 'topclassui/toast'
/** Stands in for a request: it settles after a second, one way or the other. */
const upload = (succeeds: boolean) => new Promise<number>((resolve, reject) => {
setTimeout(() => { if (succeeds) resolve(3); else reject(new Error('The server did not answer.')) }, 1000)
})
/** One toast for the whole request: a spinner, then the success or the error icon and text in its place.
* promise() hands the rejection back, so the caller catches it. */
export default function Demo() {
const send = (succeeds: boolean) => {
toast.promise(upload(succeeds), {
loading: 'Uploading',
success: (count) => ({ title: 'Uploaded', description: `${String(count)} files are in the project.` }),
error: (error) => ({ title: 'Upload failed', description: error instanceof Error ? error.message : 'Try again.' }),
}).catch(() => undefined)
}
return (
<div className="flex flex-wrap gap-2">
<Button variant="outline" onClick={() => { send(true) }}>Upload</Button>
<Button variant="outline" onClick={() => { send(false) }}>Upload and fail</Button>
</div>
)
}
'use client'
import { Button } from 'topclassui/button'
import { toast } from 'topclassui/toast'
/** duration 0: it stays until the x, a swipe or Escape closes it. */
export default function Demo() {
return (
<Button
variant="outline"
onClick={() => { toast.add({ tone: 'warning', title: 'You are offline', description: 'Changes are kept here until you reconnect.', duration: 0 }) }}
>
Go offline
</Button>
)
}
'use client'
import { Button } from 'topclassui/button'
import { toast } from 'topclassui/toast'
/** priority high: a screen reader announces it at once, as an alert, instead of after what it is reading. */
export default function Demo() {
return (
<Button
variant="outline"
onClick={() => { toast.add({ tone: 'danger', priority: 'high', title: 'Payment failed', description: 'Your card was declined.' }) }}
>
Pay now
</Button>
)
}
'use client'
import { Button } from 'topclassui/button'
import { toast } from 'topclassui/toast'
/** Twenty at once. Three show, the provider's limit; the next one appears each time one closes. */
export default function Demo() {
const flood = () => {
for (let n = 1; n <= 20; n++) toast.add({ title: `Message ${String(n)}`, duration: 0 })
}
return (
<div className="flex flex-wrap gap-2">
<Button variant="outline" onClick={flood}>Twenty messages</Button>
<Button variant="outline" onClick={() => { toast.close() }}>Close all</Button>
</div>
)
}
'use client'
import { Button } from 'topclassui/button'
import {
DialogBody, DialogClose, DialogFooter, DialogHeader, DialogPopup, DialogRoot, DialogTitle, DialogTrigger,
} from 'topclassui/dialog'
import { toast } from 'topclassui/toast'
/** A toast shown from inside a modal dialog. It is painted above the scrim, on the toast layer. */
export default function Demo() {
return (
<DialogRoot>
<DialogTrigger render={<Button variant="outline">Open settings</Button>} />
<DialogPopup>
<DialogHeader>
<DialogTitle>Settings</DialogTitle>
</DialogHeader>
<DialogBody>Saving keeps this dialog open.</DialogBody>
<DialogFooter>
<DialogClose render={<Button variant="outline">Close</Button>} />
<Button onClick={() => { toast.add({ tone: 'success', title: 'Settings saved', duration: 0 }) }}>Save</Button>
</DialogFooter>
</DialogPopup>
</DialogRoot>
)
}
'use client'
import { Button } from 'topclassui/button'
import { toast } from 'topclassui/toast'
const WORD = 'a'.repeat(300)
const PARAGRAPH = 'Every file of the project is checked again before it is published, and the ones that changed are listed here. '.repeat(4)
/** Hostile content: a 300-letter word as the title and as the text, a long action label, and three long toasts. */
export default function Demo() {
const words = () => {
toast.add({ tone: 'info', title: WORD, description: WORD, action: { label: `Open the full report of ${WORD}`, onClick: () => undefined } })
}
const tall = () => {
for (const title of ['First check', 'Second check', 'Third check']) {
toast.add({ tone: 'warning', title, description: PARAGRAPH, duration: 0 })
}
}
return (
<div className="flex flex-wrap gap-2">
<Button variant="outline" onClick={words}>One long word</Button>
<Button variant="outline" onClick={tall}>Three tall toasts</Button>
</div>
)
}
'use client'
import { Button } from 'topclassui/button'
import { TopClassProvider } from 'topclassui/provider'
import { toast } from 'topclassui/toast'
/** Arabic inside TopClassProvider. The one viewport is the outermost provider's: on an Arabic page the toast sits
* at the bottom left, and from an Arabic island on an English page it shows in the page's own corner and language. */
export default function Demo() {
return (
<TopClassProvider direction="rtl" locale="ar">
<div dir="rtl" lang="ar">
<Button
variant="outline"
onClick={() => { toast.add({ tone: 'success', title: 'تم إرسال الدعوة', description: 'تصلهم رسالة خلال دقيقة.' }) }}
>
دعوة
</Button>
</div>
</TopClassProvider>
)
}
Props
Keyboard and accessibility
- The viewport is a polite live region named by
labels.toast.region.priority="high"is read at once, as an alert: keep it for what cannot wait. - Every toast ends with an x, and nothing removes it: a swipe is a drag, and a drag needs a second way (WCAG 2.5.7).
- The timers pause while the pointer is over the toasts, while one holds focus, and while the window is in the background. Over a modal Dialog, Tab stays in the dialog and F6 reaches the toast.
Right to left
bottom-end, the default, is the bottom right in English and the bottom left in Arabic, and the sideways swipe follows the edge the toast sits at. An Arabic island on an English page shows its toasts in the page's corner, in the page's language.
Server and client
A client component: it renders on the server and hydrates in the browser.
Files: index.tsx, client.tsx.
On the chip: client - size not measured yet.