RoadieRoadie
  • Home
  • Foundations
  • Tokens
  • Components
  • Charts
  • Widgets
  • Appearance
HomeFoundationsComponents
Appearance

Components

  • Navigation

    • Breadcrumb
    • Navigator
    • Steps
    • Tabs
  • Layout

    • Pane
    • ScrollArea
    • Separator
  • Actions

    • Button
    • IconButton
    • Toggle
    • Toggle group
  • Forms

    • Forms overview
    • Autocomplete
    • Checkbox
    • Combobox
    • Field
    • Fieldset
    • Input
    • Label
    • Number field
    • OTP field
    • Radio group
    • Select
    • Slider
    • Switch
    • Textarea
  • Overlays

    • Dialog
    • Drawer
    • Menu
    • Popover
    • Toast
    • Tooltip
  • Collections

    • Accordion
    • Card
    • Carousel
    • Collapsible
    • DataTable
    • List
    • Marquee
    • Table
  • Status

    • Badge
    • Callout
    • Countdown
    • Empty state
    • Meter
    • Progress
    • Skeleton
    • Sparkline
    • StatTile
  • Media & brand

    • Avatar
    • Icon Tile
    • Image
    • Logo
    • QR code
    • SpotIllustration
  • Text

    • CalendarTile
    • Code
    • DateTime
    • Duration
    • Highlight
    • Mark
    • Prose

Toast

A brief message that confirms an action or reports its result, then gets out of the way.

Import

import { Toast, useToastManager } from '@oztix/roadie-components/toast'

Setup

RoadieProvider mounts Toast.Provider and a Toast.Viewport at your app root, so toasts work once you've followed Installation. The viewport portals to the end of the page. Toasts stack at the bottom end of wide screens and span the bottom of small ones. See Position to change that.

<RoadieProvider link={NextLink} toast={{ position: 'top-end' }}>
{children}
</RoadieProvider>

Placing providers yourself? Mount one Toast.Provider at the root with a Toast.Viewport inside it.

toast takes Toast.Provider's timeout (5000ms by default) and limit (3 on screen by default) for every toast in the app. This site mounts one, so the examples below work as they would in your app.

Examples

Default

Call useToastManager() in any component under the provider, then add a toast. A title on its own is often enough. A thin bar along the bottom shows how long the toast has left.

function CopyLink() {
  const toast = useToastManager()
  return (
    <Button onClick={() => toast.add({ title: 'Link copied' })}>
      <LinkSimpleIcon weight='bold' className='size-4' />
      Copy link
    </Button>
  )
}
render(<CopyLink />)

Intents

Pass intent to colour the toast and lead with a matching icon. Without one it stays neutral, with no icon.

function Intents() {
  const toast = useToastManager()
  const toasts = {
    success: ['Tickets sent', 'Check your inbox for 2 tickets.'],
    info: ['Doors open at 7pm', 'Arrive early for the support act.'],
    warning: ['Only 12 tickets left', 'Your cart is held for 10 minutes.'],
    danger: ['Card declined', 'Try another card or contact your bank.']
  }
  return (
    <div className='flex flex-wrap gap-2'>
      {Object.entries(toasts).map(([intent, [title, description]]) => (
        <Button
          key={intent}
          intent={intent}
          onClick={() => toast.add({ title, description, intent })}
        >
          {intent}
        </Button>
      ))}
    </div>
  )
}
render(<Intents />)

Position

Set position once for the whole app, in RoadieProvider's toast or on Toast.Viewport: bottom-end (the default), bottom-center, top-end or top-center. end follows the reading direction. On small screens toasts always span the chosen edge. Top toasts slide in from the top, stack downwards and swipe up to dismiss.

These buttons each use their own provider so you can compare them. Your app mounts one.

function Show({ position }) {
  const toast = useToastManager()
  return (
    <Button onClick={() => toast.add({ title: 'Link copied', description: position })}>
      {position}
    </Button>
  )
}
function Positions() {
  return (
    <div className='flex flex-wrap gap-2'>
      {['bottom-end', 'bottom-center', 'top-end', 'top-center'].map((position) => (
        <Toast.Provider key={position}>
          <Show position={position} />
          <Toast.Viewport position={position} />
        </Toast.Provider>
      ))}
    </div>
  )
}
render(<Positions />)

Clearing sticky UI

A fixed bar, cookie banner or chat button can cover the toasts. Set --toast-viewport-offset-bottom to its height, on Toast.Viewport or any ancestor, and the stack moves up by that much. Top positions read --toast-viewport-offset-top. Reach for the offset before moving toasts to the top.

In a Navigator app, top toasts already clear the tallest Pane.Header along the top of the window and follow it as it collapses. Set --toast-viewport-offset-top only for other fixed UI; it adds to that.

function Checkout() {
  const [barShown, setBarShown] = useState(false)
  return (
    <Toast.Provider>
      <div className='flex flex-wrap gap-2'>
        <Button onClick={() => setBarShown(!barShown)}>
          {barShown ? 'Hide' : 'Show'} checkout bar
        </Button>
        <SaveForLater />
      </div>
      {barShown &&
        createPortal(
          <div
            data-checkout-bar
            className='fixed inset-x-0 bottom-0 z-50 flex h-20 items-center justify-end px-6 emphasis-raised'
          >
            <Button>Continue</Button>
          </div>,
          document.body
        )}
      <Toast.Viewport
        style={{ '--toast-viewport-offset-bottom': barShown ? '5rem' : '0px' }}
      />
    </Toast.Provider>
  )
}
function SaveForLater() {
  const toast = useToastManager()
  return (
    <Button onClick={() => toast.add({ title: 'Saved for later', intent: 'success' })}>
      Save for later
    </Button>
  )
}
render(<Checkout />)

In your own CSS you can set it wherever the bar appears:

:root:has([data-checkout-bar]) {
--toast-viewport-offset-bottom: 5rem;
}

Timing

A toast stays for the provider's timeout, 5000ms by default, or its own timeout if you pass one. The bar along its bottom empties in that time. Hovering or focusing the toasts pauses every timer and the bar with it, and they carry on from the same point afterwards. They also pause while the window is in the background. F6 moves focus to the toasts from anywhere on the page.

timeout: 0 keeps a toast until it's dismissed and drops the bar. So does a promise toast while it's loading. It starts its timer when the work settles.

With an action

actionProps adds a small button. It takes any button props, with the label as children. Keep it to one short verb, such as Undo. A toast with a convenience action like Undo still times out, but give it longer to read and reach, about 8000ms.

function SaveEvent() {
  const toast = useToastManager()
  function save() {
    toast.add({
      title: 'Event saved',
      description: 'Midnight Static at The Lantern Room, Fitzroy',
      intent: 'success',
      timeout: 8000,
      actionProps: {
        children: 'Undo',
        onClick: () => toast.add({ title: 'Changes undone' })
      }
    })
  }
  return <Button onClick={save}>Save event</Button>
}
render(<SaveEvent />)

When the action is the only way forward, such as Retry, set timeout: 0 so the toast waits for them. If the problem needs more than a retry, show a Callout next to it instead.

function SaveTicketTypes() {
  const toast = useToastManager()
  function fail() {
    toast.add({
      title: 'Ticket types not saved',
      description: 'We lost the connection. Your changes are still here.',
      intent: 'danger',
      timeout: 0,
      actionProps: {
        children: 'Retry',
        onClick: () => toast.add({ title: 'Ticket types saved', intent: 'success' })
      }
    })
  }
  return <Button onClick={fail}>Save ticket types</Button>
}
render(<SaveTicketTypes />)

Promise

promise shows a spinner while the work runs, then swaps in the success or error toast. It returns the promise, so you can still await the result.

function PublishEvent() {
  const toast = useToastManager()
  function publish(succeeds) {
    const request = new Promise((resolve, reject) =>
      setTimeout(
        () => (succeeds ? resolve('Harbourlight Sessions') : reject()),
        2000
      )
    )
    toast.promise(request, {
      loading: 'Publishing Harbourlight Sessions',
      success: (name) => ({
        title: `${name} is live`,
        description: 'Tickets go on sale Friday at 9am.'
      }),
      error: {
        title: 'Harbourlight Sessions not published',
        description: 'We lost the connection. Try again in a moment.'
      }
    })
  }
  return (
    <div className='flex flex-wrap gap-2'>
      <Button onClick={() => publish(true)}>Publish event</Button>
      <Button onClick={() => publish(false)}>Publish, then fail</Button>
    </div>
  )
}
render(<PublishEvent />)

If the work fails because something needs fixing, such as missing event details, show a Callout next to the problem instead. It stays until the problem is fixed.

Updating a toast

add returns an id. Pass it to update to change the toast in place, or to close to dismiss it.

function Upload() {
  const toast = useToastManager()
  function upload() {
    const id = toast.add({ title: 'Uploading poster', timeout: 0 })
    setTimeout(
      () =>
        toast.update(id, {
          title: 'Poster uploaded',
          intent: 'success',
          timeout: 5000
        }),
      1500
    )
  }
  return <Button onClick={upload}>Upload poster</Button>
}
render(<Upload />)

Outside React

createToastManager makes a manager you can call from anywhere, such as a fetch wrapper or a store. Pass it to Toast.Provider. It has the same add, update, close and promise as the hook.

// toasts.ts
import { createToastManager } from '@oztix/roadie-components/toast'
export const toasts = createToastManager()
// providers.tsx
<Toast.Provider toastManager={toasts}>
{children}
<Toast.Viewport />
</Toast.Provider>
// api.ts
toasts.add({ title: 'Session expired', intent: 'warning' })

Composition

Toast.Viewport renders every toast for you. To lay a toast out differently, pass your own list as children and build each one from its parts. Toast.Title, Toast.Description and Toast.Action fill themselves from the toast, Toast.Icon picks the intent's icon, and Toast.Progress draws the time-left bar.

function Toasts() {
const { toasts } = useToastManager()
return toasts.map((toast) => (
<Toast key={toast.id} toast={toast}>
<Toast.Content>
<Toast.Icon />
<div className='grid min-w-0 flex-1 gap-0.5'>
<Toast.Title />
<Toast.Description />
</div>
<Toast.Action />
<Toast.Close />
<Toast.Progress />
</Toast.Content>
</Toast>
))
}
<Toast.Viewport>
<Toasts />
</Toast.Viewport>

Guidelines

Only for news that is safe to miss

Do

Use a toast to confirm something the person just did, or to report how background work finished. Say what happened in a few words: "Link copied", or "Saved" with Undo.

Don’t

Don't put anything the person must see or act on in a toast. It disappears on its own. If an event can't publish until details are fixed, show a Callout next to the problem. Use a Dialog for a decision and inline errors for a form.

One position for the whole app

Do

Keep the default bottom end unless fixed UI collides with it. Try --toast-viewport-offset-bottom first, and only move to the top if the offset can't clear it.

Don’t

Don't change the position per page or per toast. People learn where to look.

One action at most

timeout: 8000,
actionProps: { children: 'Undo', onClick: undo }

Do

Offer one quick follow-up, like Undo. Let it time out, with about 8000ms so there's time to reach it, and make sure the change can also be reversed somewhere else, such as a bin or history. Hovering or focusing the toast pauses it for anyone who needs longer.

Don’t

Don't stack several buttons in a toast. If the action is the only way forward, like Retry, View order or Update card, set timeout: 0, or better, show a Callout next to the problem.

Match the intent to the outcome

Do

Use success for work that finished, danger for work that failed, and warning for something that needs attention soon. Leave simple confirmations neutral.

Don’t

Don't use danger for validation. Show that next to the field.

Accessibility

  • Toasts are announced politely by a live region. Pass priority: 'high' for an urgent message so it is announced straight away.
  • F6 moves focus to the toast region. Tab moves through the toasts' actions and dismiss buttons, and Escape dismisses the focused toast.
  • Hovering or focusing the region fans the stack out and pauses every timer.
  • The time-left bar is hidden from screen readers. With reduced motion it isn't shown, since it can't empty smoothly.
  • Swipe towards the viewport's edge, or sideways, to dismiss on touch.
  • With reduced motion, toasts appear and leave without sliding.
  • The dismiss button is labelled Dismiss. Pass aria-label to Toast.Close to change it.

Hooks

useToastManager

function useToastManager(): UseToastManagerReturnValue

Reads the nearest Toast.Provider. createToastManager() returns the same methods, without toasts.

FieldTypeDescription
toastsToastObject[]The toasts on screen, newest first.
add(options: ToastAddOptions) => stringShow a toast. Returns its id.
update(id: string, options: ToastUpdateOptions) => voidChange a toast in place.
close(id?: string) => voidDismiss one toast, or all of them without an id.
promise(promise, { loading, success, error }) => PromiseShow a spinner until promise settles, then its result.

add takes title, description, intent, actionProps, timeout, priority, onClose and data. Pass an existing id to replace that toast.

API reference

Toast

toastToastRootToastObject<any>
Required

The toast to render.

swipeDirection"up" | "down" | "left" | "right" | ("up" | "down" | "left" | "right")[]

Direction(s) in which the toast can be swiped to dismiss.

Defaults to ['down', 'right'].

classNamestring | ((state: ToastRootState) => string)

CSS class applied to the element, or a function that returns a class based on the component's state.

Toast.Action

A small Button built from the toast's `actionProps`. Renders nothing without a label.

classNamestring | ((state: ToastActionState) => string)

CSS class applied to the element, or a function that returns a class based on the component's state.

Inherited from NativeButtonProps

nativeButtonboolean

Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`).

Defaults to true.

Toast.Close

An icon button that dismisses the toast.

classNamestring | ((state: ToastCloseState) => string)

CSS class applied to the element, or a function that returns a class based on the component's state.

Inherited from NativeButtonProps

nativeButtonboolean

Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`).

Defaults to true.

Toast.Content

Lays out a toast's parts in a row. Fades out while stacked behind the front toast.

No additional props. It forwards all standard HTML attributes to the underlying element.

Toast.Description

Shows the toast's `description` unless you pass children.

No additional props. It forwards all standard HTML attributes to the underlying element.

Toast.Icon

The toast's intent icon, or a spinner while loading. Pass children to use your own icon.

No additional props. It forwards all standard HTML attributes to the underlying element.

Toast.Progress

A thin bar along the bottom of a timed toast that empties as its time runs out. It pauses whenever the toast's timer does: while the toasts are hovered or focused, or the window is in the background. Renders nothing for a toast that won't close on its own.

No additional props. It forwards all standard HTML attributes to the underlying element.

Toast.Provider

Holds the app's toasts. Mount once at the root.

toastManager(Omit<ToastManager<any>, "add" | "update"> & ToastIntentMethods)

A manager from `createToastManager`, to add toasts from outside React.

Inherited from ToastProviderProps

timeoutnumber

The default amount of time (in ms) before a toast is auto dismissed. A value of `0` will prevent the toast from being dismissed automatically.

Defaults to 5000.

limitnumber

The maximum number of toasts that can be displayed at once. When the limit is exceeded, the oldest toasts are marked as `limited` (via the `data-limited` attribute) rather than removed, so they can be hidden or animated out.

Defaults to 3.

Toast.Title

Shows the toast's `title` unless you pass children.

No additional props. It forwards all standard HTML attributes to the underlying element.

Toast.Viewport

The fixed region toasts stack in, at `position` on wider screens and full width along the same edge on small ones. Set `--toast-viewport-offset-bottom` (or `-top`) on it or an ancestor to clear fixed UI. Renders every toast unless you pass children.

classNamestring | ((state: ToastViewportState) => string)

CSS class applied to the element, or a function that returns a class based on the component's state.

containerHTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null

Where the viewport portals to.

Defaults to document.body.

position"bottom-end" | "bottom-center" | "top-end" | "top-center"

The edge toasts stack at, set once for the whole app. `end` follows the reading direction. Small screens always span the chosen edge.

Defaults to bottom-end.

Previous page← PopoverNext pageTooltip →