@remcostoeten/notifier
Composable toast notifications for React.
Toasts built from slots: an icon or avatar, a title, a description, progress and actions. Open one with a single call, update it in place, or compose the parts for your own layout.
Live demo
Save the form to see one notification move from loading to success.
1'use client'23import { Notifier, notify } from '@remcostoeten/notifier'45export function SaveButton() {6 return (notify opens a toast and returns its id. notify.update and notify.promise change that toast in place, so one job stays one element. For layouts the slots do not cover, notify.custom renders the Toast parts directly. There is no stylesheet to import; Notifier adds its own.
One toast moves from loading to success or error when the promise settles.
1await notify.promise(saveSettings(), {2 loading: 'Saving settings…',3 success: data => ({ title: 'Settings saved', description: data.name }),4 error: error => ({ title: 'Could not save', description: error.message })Pass the slots as one object. Filled with the accent color is the primary button.
1notify({2 tone: 'dark',3 icon: <Download />,4 title: 'A new update is available',5 meta: 'v4.2',6 description: 'Includes the new dashboard view.',7 actions: [8 { label: 'Skip', variant: 'outline', color: 'muted' },9 { label: 'Install now', variant: 'filled', color: 'accent', onClick: install }10 ]11})Keep the id and patch the same toast. At 100 it starts its timer and closes.
1const id = notify({ title: 'Uploading', progress: 0 })23upload.on('progress', percent => notify.update(id, { progress: percent }))4upload.on('done', () => notify.update(id, { title: 'Uploaded', progress: 100 }))The Toast parts read tone and dismiss handling from the toast around them.
1notify.custom(() => (2 <Toast>3 <Toast.Avatar src={sender.avatar} badge="online" />4 <Toast.Body>5 <Toast.Title meta="2 mins ago">{sender.name}</Toast.Title>6 <Toast.Description>{message.preview}</Toast.Description>7 <Toast.Actions>8 <Toast.Action variant="outline">Dismiss</Toast.Action>9 <Toast.Action variant="filled" color="accent" onClick={reply}>10 Reply11 </Toast.Action>12 </Toast.Actions>13 </Toast.Body>14 <Toast.Close />15 </Toast>16))const id = notify(title, input?)input.status'success', 'error', 'warning', 'info' or 'loading'. Adds the matching icon.input.progress0 to 100, or null for indeterminate. Keeps the toast open until it reaches 100.input.actionsButtons or links. Each closes the toast after onClick unless dismiss is false.input.durationMilliseconds before it closes. 0 keeps it open.input.idReuse an id to replace that toast instead of opening another.<Notifier position="bottom-right" tone="auto" />tone'light', 'dark' or 'auto', which follows prefers-color-scheme.maxVisibleToasts per position. Loading and unfinished progress toasts are never replaced.stackOverlaps toasts into a stack that expands on hover.themeFont, radius, width and color tokens per tone, also settable as --ntf-* CSS variables.layerPuts the stylesheet in a cascade layer so Tailwind v4 utilities can override it.