Qure UI
Components

Toast

A transient message raised from anywhere, rendered somewhere else.

'use client'import { Button } from '@/registry/qure/ui/button'import { Toaster, ToastProvider, useToast } from '@/registry/qure/ui/toast'/** * In an app the Provider and the Toaster go in the root layout, once. They * are here so the demo is a single file you can run. */export default function ToastDemo() {  return (    <ToastProvider>      <SignButton />      <Toaster />    </ToastProvider>  )}function SignButton() {  const toast = useToast()  return (    <Button      onClick={() =>        toast.add({          type: 'success',          title: 'Report signed and sent',          description: 'ACC-4471902 is with the referring clinician.',        })      }    >      Sign and send    </Button>  )}

Installation

npx shadcn@latest add https://qure-ui.qure.ai/r/toast.json

Usage

Render the provider and the viewport once, near the root of the app:

import { Toaster, ToastProvider } from '@/components/ui/toast'

export default function RootLayout({ children }) {
  return (
    <ToastProvider>
      {children}
      <Toaster />
    </ToastProvider>
  )
}

Then raise a toast from anywhere inside it:

import { useToast } from '@/components/ui/toast'

const toast = useToast()

toast.add({
  type: 'success',
  title: 'Report signed and sent',
  description: 'ACC-4471902 is with the referring clinician.',
})

Toasts are not rendered by the code that raises them. add() pushes an object into a store the provider owns, and the Toaster renders whatever is in it. That indirection is the point: an upload that finishes after the user has navigated away still has somewhere to say so, and no component has to stay mounted to hold the message.

A toast is the least reliable place to put information. It is transient, it can be missed entirely, and a screen reader announces it once. Anything a clinician must act on belongs on the page as well — use an Alert or a field error, and treat the toast as the reminder rather than the record.

Composition

<ToastProvider>     {/* owns the store; limit, timeout, toastManager */}
  …your app…
  <Toaster />       {/* portal + viewport + the stack */}
</ToastProvider>

Toaster renders a role="region" labelled "Notifications" with a polite live region inside it, all of which arrives with the primitive. Hovering or focusing the viewport expands the stack from a peeked pile into a readable list and stops the timers, so a toast cannot expire while it is being read.

Toast itself reads everything it shows off the object the manager handed it — title, description, actionProps, type. Each part renders nothing at all when its field is absent, so a bare add({ description }) does not leave an empty heading behind.

Tones

'use client'import { Button } from '@/registry/qure/ui/button'import { Toaster, ToastProvider, useToast, type ToastTone } from '@/registry/qure/ui/toast'const messages: { tone: ToastTone; title: string; description: string }[] = [  { tone: 'info', title: 'Study queued', description: 'ACC-4471902 is 3rd in the qXR queue.' },  { tone: 'success', title: 'Report signed and sent', description: 'Delivered to the respiratory clinic.' },  { tone: 'attention', title: 'Prior study is 14 months old', description: 'Comparison may be unreliable.' },  { tone: 'urgent', title: 'Failed to retrieve prior study', description: 'The archive did not respond.' },]/** * `type` is a free string in Base UI — it lands on the root as `data-type` * and the stylesheet does the rest. These four are the design system's * statuses, the same four Badge and Alert use. */export default function ToastTone() {  return (    <ToastProvider limit={4}>      <ToneButtons />      <Toaster />    </ToastProvider>  )}function ToneButtons() {  const toast = useToast()  return (    <div className="flex flex-wrap justify-center gap-2">      {messages.map((m) => (        <Button key={m.tone} variant="secondary" size="sm" onClick={() => toast.add(m)}>          {m.tone}        </Button>      ))}    </div>  )}

type is a free string in Base UI: it lands on the root as data-type and the stylesheet does the rest. These four are the design system's statuses — the same four Badge and Alert use — and each carries the icon Figma's Status bar pairs with it, so a success reads the same in a bar and in a toast.

With an action

'use client'import * as React from 'react'import { Button } from '@/registry/qure/ui/button'import { Toaster, ToastProvider, useToast } from '@/registry/qure/ui/toast'export default function ToastAction() {  return (    <ToastProvider>      <RetryButton />      <Toaster />    </ToastProvider>  )}/** * A failure the user can do something about. The action is passed as * `actionProps` — anything a `<button>` takes — and Base UI renders nothing * at all when it is absent, so the same toast component serves both cases. * * Giving the toast a fixed `id` means a second failure updates the one * already on screen and restarts its timer, rather than stacking three * copies of the same sentence. */function RetryButton() {  const toast = useToast()  const [attempt, setAttempt] = React.useState(0)  function fail() {    setAttempt((n) => n + 1)    toast.add({      id: 'prior-fetch',      type: 'urgent',      priority: 'high',      title: 'Failed to retrieve prior study',      description: 'ACC-4409123 did not come back from the archive.',      actionProps: { children: 'Retry', onClick: fail },    })  }  return (    <div className="flex flex-col items-center gap-2">      <Button variant="secondary" onClick={fail}>Fetch prior study</Button>      {attempt > 0 ? (        <p className="text-xs text-muted-foreground">Attempt {attempt}. Retry updates the same toast.</p>      ) : null}    </div>  )}

Pass actionProps — anything a <button> takes. Give the toast a fixed id and a second failure updates the one already on screen and restarts its timer, rather than stacking three copies of the same sentence:

toast.add({
  id: 'prior-fetch',
  type: 'urgent',
  priority: 'high',
  title: 'Failed to retrieve prior study',
  actionProps: { children: 'Retry', onClick: fail },
})

An action is also the honest response to priority: 'high'. If the message is urgent enough to interrupt, it should usually offer the thing to do about it.

Async work

'use client'import { Button } from '@/registry/qure/ui/button'import { Toaster, ToastProvider, useToast } from '@/registry/qure/ui/toast'export default function ToastPromise() {  return (    <ToastProvider>      <UploadButton />      <Toaster />    </ToastProvider>  )}/** * One toast for the whole operation. `promise()` shows the loading message * with no timeout, then rewrites the same toast when the promise settles — * the alternative is a "Uploading…" that expires while the upload is still * running and a second toast appearing from nowhere to contradict it. */function UploadButton() {  const toast = useToast()  function upload() {    const work = new Promise<{ count: number }>((resolve, reject) => {      setTimeout(() => (Math.random() > 0.35 ? resolve({ count: 412 }) : reject(new Error('Connection reset'))), 1800)    })    toast.promise(work, {      loading: { title: 'Uploading series', description: 'ACC-4471902 — CT Thorax.' },      success: (result) => ({        type: 'success',        title: 'Series uploaded',        description: `${result.count} slices are in the archive.`,      }),      error: (error) => ({        type: 'urgent',        title: 'Upload failed',        description: `${error.message}. Nothing was written to the archive.`,        actionProps: { children: 'Retry', onClick: upload },      }),    })  }  return <Button variant="secondary" onClick={upload}>Upload series</Button>}

promise() shows the loading message with no timeout, then rewrites the same toast when the promise settles:

toast.promise(work, {
  loading: { title: 'Uploading series' },
  success: result => ({ type: 'success', title: `${result.count} slices are in the archive.` }),
  error: error => ({ type: 'urgent', title: 'Upload failed', description: error.message }),
})

Doing it by hand gives you an "Uploading…" that expires while the upload is still running, and then a second toast appearing from nowhere to contradict it.

Stacking and sticky toasts

'use client'import * as React from 'react'import { Button } from '@/registry/qure/ui/button'import { Toaster, ToastProvider, useToast } from '@/registry/qure/ui/toast'export default function ToastStack() {  return (    <ToastProvider limit={3} timeout={8000}>      <StackButtons />      <Toaster position="top-right" />    </ToastProvider>  )}/** * Raise several and hover the pile: Base UI expands the stack and stops the * timers while the pointer or focus is in the viewport, so a toast cannot * expire while it is being read. * * `timeout: 0` opts one toast out of dismissal entirely. Use it only where * the message must be acknowledged — here, a study that will not be read * unless somebody reassigns it. */function StackButtons() {  const toast = useToast()  const n = React.useRef(0)  return (    <div className="flex flex-wrap justify-center gap-2">      <Button        variant="secondary"        onClick={() => {          n.current += 1          toast.add({            type: 'success',            title: `Report signed and sent`,            description: `ACC-44719${String(n.current).padStart(2, '0')} is with the referring clinician.`,          })        }}      >        Raise one      </Button>      <Button        variant="tertiary"        onClick={() =>          toast.add({            type: 'urgent',            timeout: 0,            priority: 'high',            title: 'Unassigned urgent study',            description: 'ACC-4471902 has been unread for 42 minutes.',            actionProps: { children: 'Assign' },          })        }      >        Raise a sticky one      </Button>    </div>  )}

limit on the provider caps how many are on screen; timeout sets the default dwell. position on the Toaster moves the pile — and moves the swipe direction with it, so a toast pinned to the top cannot be dismissed by dragging it down into the page.

timeout: 0 opts a single toast out of dismissal entirely. Use it only where the message must be acknowledged — an unassigned urgent study, not a saved draft.

Outside React

Some of the places worth raising a toast from are not components: an API client, a websocket handler, a route guard. Create a manager outside the tree and hand it to the provider:

import { createToastManager } from '@/components/ui/toast'

export const toast = createToastManager()
<ToastProvider toastManager={toast}>

toast.add(...) then works from anywhere, hook or not.

API Reference

ToastProvider

PropTypeDefaultDescription
limitnumber3How many toasts are on screen at once.
timeoutnumber5000Default dwell in milliseconds. 0 never dismisses.
toastManagerToastManagerA manager from createToastManager, for raising toasts outside React.

Toaster

PropTypeDefaultDescription
position"bottom-right" | "bottom-center" | "top-right" | "top-center""bottom-right"Where the stack sits. Also sets the swipe direction.

useToast

Returns the manager: add, update, close, promise, and the live toasts list.

add(options) takes title, description, type, id, timeout, priority, actionProps and the rest of Base UI's toast options. See the Base UI documentation for the full set.

On this page