Monet
@etched/monet 0.0.1

Toast

Ported

A manager for code outside React (timers, fetch helpers). Pass it to <Toasty toastManager={…}>.

Kumo source
import { Button } from "@etched/monet/button";
import { useToastManager } from "@etched/monet/toast";

export function Preview() {
  const toasts = useToastManager();

  return (
    <Button onClick={() => toasts.add({ variant: "success", title: "Run 1,284 finished", description: "All 96 chips passed." })}>
      Finish run
    </Button>
  );
}

Installation

import { createToastManager, Toasty, useToastManager } from "@etched/monet/toast";

Examples

Variants

Error toasts stay until the user closes them. The default toast has an id, so a second click bumps the same toast.

import { Button } from "@etched/monet/button";
import { useToastManager } from "@etched/monet/toast";

export function Variants() {
  const toasts = useToastManager();

  return (
    <div className="row">
      {/* Orbit's intake toast. */}
      <Button onClick={() => toasts.add({ id: "intake", title: "Candidate saved." })}>Default</Button>
      <Button onClick={() => toasts.add({ variant: "success", title: "Run 1,284 finished", description: "All 96 chips passed." })}>
        Success
      </Button>
      <Button onClick={() => toasts.add({ variant: "info", title: "New results", description: "Lot 7 has 12 new chips." })}>Info</Button>
      <Button onClick={() => toasts.add({ variant: "warning", title: "Queue almost full", description: "New runs may start late." })}>
        Warning
      </Button>
      <Button onClick={() => toasts.add({ variant: "error", title: "Owner not saved", description: "The server did not answer. Try again." })}>
        Error
      </Button>
    </div>
  );
}

Loading, promise and retry

A loading toast shows a spinner and stays until update changes it. promise does the same for one promise. An error that is updated to success closes on the usual timer again.

import { Button } from "@etched/monet/button";
import { useToastManager } from "@etched/monet/toast";

const wait = (ms: number, fail = false) => new Promise<void>((resolve, reject) => setTimeout(fail ? reject : resolve, ms));

export function LoadingPromiseAndRetry() {
  const toasts = useToastManager();

  const save = () => {
    const id = toasts.add({ variant: "loading", title: "Saving owner…" });
    setTimeout(() => toasts.update(id, { variant: "success", title: "Owner saved" }), 1000);
  };

  const failThenRetry = () => {
    const retry = () => {
      toasts.update(id, { variant: "loading", title: "Saving notes…", description: undefined, actions: [] });
      setTimeout(() => toasts.update(id, { variant: "success", title: "Notes saved" }), 1000);
    };

    const id = toasts.add({
      variant: "error",
      title: "Notes not saved",
      description: "The server did not answer.",
      actions: [{ children: "Retry", size: "sm", onClick: retry }],
    });
  };

  const exportRuns = (fail: boolean) =>
    toasts
      .promise(wait(1500, fail), {
        loading: { title: "Exporting 96 runs…" },
        success: { title: "Runs exported", description: "runs-1284.csv is in your downloads." },
        error: { title: "Export failed", description: "Lot L-7799 has no runs. Check the lot ID and try again." },
      })
      // The toast shows the failure.
      .catch(() => {});

  return (
    <div className="row">
      <Button onClick={save}>Saving, then saved</Button>
      <Button onClick={() => exportRuns(false)}>Export runs</Button>
      <Button onClick={() => exportRuns(true)}>Export that fails</Button>
      <Button onClick={failThenRetry}>Fails, then retry</Button>
    </div>
  );
}

Action and persistent

Actions do not close the toast; call close(id) for that. A timeout of 0 keeps a toast until the user closes it.

import { Button } from "@etched/monet/button";
import { useToastManager } from "@etched/monet/toast";

export function ActionAndPersistent() {
  const toasts = useToastManager();

  const deleteRun = () => {
    const id = toasts.add({
      title: "Run 1,284 deleted",
      actions: [{ children: "Undo", size: "sm", onClick: () => toasts.close(id) }],
    });
  };

  return (
    <div className="row">
      <Button onClick={deleteRun}>With action</Button>
      <Button onClick={() => toasts.add({ variant: "info", title: "Firmware 2.4 is out", description: "Stays until you close it.", timeout: 0 })}>
        Persistent
      </Button>
    </div>
  );
}

Accessibility

  • F6 moves focus to the toasts; Tab then reaches each toast and its buttons. Escape closes the focused toast, and Shift+Tab goes back.
  • A polite region named "Notifications" holds them. Each toast is a dialog, or an alertdialog for error; closeLabel names its close button.
  • Screen readers hear new toasts politely. An error toast is read at once from an alert region.
  • Timers pause while the pointer or keyboard focus is on the toasts. error and loading toasts stay until closed.
  • Do not make a toast's action, such as Undo, the only way to do it: most toasts close after timeout. Pass translated text in closeLabel.

API reference

PropTypeDefaultDescription
children *ReactNode–The app. Toasts render in a portal above it.
toastManagerToastManager–A manager from createToastManager(), so code outside React can show toasts here.
limitnumber3Most toasts shown at once. Older ones fade out when a new one comes.
timeoutnumber5000Milliseconds before a toast closes, when it gives no timeout. 0 keeps toasts open. Error and loading toasts stay open anyway.
closeLabelstring"Close"Names each toast's close button. Pass translated text.
containerBaseToast.Portal.Props["container"]document.bodyThe element the toasts render into (a portal), for example a shadow root.