Toast

A hand-drawn Base UI toast styled with shadcn conventions. Toasts stack from the bottom right, expand on hover, and can be swiped away.

Toast
Hand-drawn toasts that stack, expand on hover, and swipe away.
Types
Paper texturesThe pattern takes the toast type colour
Plain
With action
StackingHover the stack to expand it

Installation

npx shadcn@latest add https://sketchcn.chuwii.com/r/toast.json

Styles

Toast leans on these rules from sketch.css. The shadcn CLI installs the file with sketch-provider; copying the component by hand means copying them too.

@keyframes sketch-icon-wiggle {
  0%,
  100% {
    transform: rotate(-5deg);
  }

  50% {
    transform: rotate(5deg);
  }
}

[data-paper]:not([data-paper="default"])::before,
[data-slot="card"][data-variant]:not([data-variant="default"])::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-color: var(--paper-pattern);
  opacity: var(--paper-opacity, 0.4);
  mask-image: var(--paper-image);
  -webkit-mask-image: var(--paper-image);
  mask-repeat: repeat;
  -webkit-mask-repeat: repeat;
}

Read the whole file for the drawing variables, paper textures and reduced-motion rules.

Usage

Render <Toaster /> once near the root of your app. It portals its own viewport, so no other wiring is needed.

import { Toaster } from "@/components/ui/toast";

export function App({ children }: { children: React.ReactNode }) {
  return (
    <>
      {children}
      <Toaster />
    </>
  );
}

Then call toast from anywhere.

import { Button } from "@/components/ui/button";
import { toast } from "@/components/ui/toast";

export function SaveButton() {
  return (
    <Button
      onClick={() =>
        toast.add({
          type: "success",
          title: "Sketch saved",
          description: "Everyone on the board can see it now.",
        })
      }
    >
      Save sketch
    </Button>
  );
}

Parts

PartDescription
ToasterProvider, portal, viewport and toast list in one. Mount it once.
ToastA single outlined toast.
ToastContentRow holding the icon, text and controls.
ToastTitleAccessible name, filled from title.
ToastDescriptionSupporting text, filled from description.
ToastActionAction button, rendered from actionProps. Defaults to an outline button.
ToastCloseDismiss button. Defaults to a ghost icon button.
ToastProviderProvider only, if you compose the pieces yourself.
ToastPortalPortal only.
ToastViewportFixed bottom-right stack.

Options

Options passed to toast.add are the Base UI toast options, with one sketch addition.

OptionTypeDefaultDescription
data{ variant?: PaperVariant }—variant draws a paper texture behind the toast.

type picks the leading icon and tint from the sketch icon set.

PaperVariant is "default" | "polkadots" | "hexagons" | "graph" | "plus" | "filled-dots".

Examples

Setup

Render <Toaster /> once near the root of your app. It portals its own viewport, so no other wiring is needed.

import { Toaster } from "@/components/ui/toast";

export function App({ children }: { children: React.ReactNode }) {
  return (
    <>
      {children}
      <Toaster />
    </>
  );
}

Save confirmation

import { Button } from "@/components/ui/button";
import { toast } from "@/components/ui/toast";

export function SaveButton() {
  return (
    <Button
      onClick={() =>
        toast.add({
          type: "success",
          title: "Sketch saved",
          description: "Everyone on the board can see it now.",
        })
      }
    >
      Save sketch
    </Button>
  );
}

Undo an action

import { Button } from "@/components/ui/button";
import { toast } from "@/components/ui/toast";

export function DeleteButton() {
  return (
    <Button
      variant="destructive"
      onClick={() =>
        toast.add({
          title: "Sketch deleted",
          description: "It moves to the bin for 30 days.",
          actionProps: { children: "Undo", onClick: restoreSketch },
        })
      }
    >
      Delete sketch
    </Button>
  );
}

Paper texture

import { Button } from "@/components/ui/button";
import { toast } from "@/components/ui/toast";

export function PublishButton() {
  return (
    <Button
      onClick={() =>
        toast.add({
          type: "warning",
          title: "Running low on space",
          description: "You have used 92% of your workspace.",
          data: { variant: "plus" },
        })
      }
    >
      Publish
    </Button>
  );
}

Promise

import { Button } from "@/components/ui/button";
import { toast } from "@/components/ui/toast";

export function ExportButton() {
  return (
    <Button
      onClick={() =>
        toast.promise(exportSketch(), {
          loading: "Exporting sketch",
          success: "Export ready",
          error: "Export failed",
        })
      }
    >
      Export PNG
    </Button>
  );
}

Stays until dismissed

import { Button } from "@/components/ui/button";
import { toast } from "@/components/ui/toast";

export function ConnectionToast() {
  return (
    <Button
      variant="outline"
      onClick={() =>
        toast.add({
          type: "warning",
          title: "You are offline",
          description: "Changes are queued until the connection is back.",
          timeout: 0,
          priority: "high",
        })
      }
    >
      Go offline
    </Button>
  );
}

Animation

Hovering a toast wiggles its icon with the sketch-icon-wiggle keyframe from sketch.css, and a loading toast spins its icon instead.

"group-hover/toast:[&_svg]:animate-[sketch-icon-wiggle_180ms_ease-in-out_infinite] motion-reduce:group-hover/toast:[&_svg]:animate-none"

The variant prop sets data-paper on the toast, so --paper-pattern and --paper-opacity tune the texture.

toast("Saved", { variant: "graph" });