Skip to content

FeedbackToast

Toast

A transient confirmation or error message that floats above the page. Used after a form submit, a save, or a destructive action.

4-second defaultBottom-right placementStacks vertically

Demo

The toast appears bottom-right, slides in, persists for 4 seconds, then slides out. The user can dismiss it earlier with the × button or by clicking outside.

Tones

Saved

Could not save

Try again.

Anatomy

  • 320 px max width, 4 px radius, 1 px --border on three sides, 3 px on the left in the tone color
  • --surface-elevated background with elevation-2 shadow
  • Title 0.875 rem, body 0.8125 rem muted
  • Slide-in: 200 ms ease-out. Slide-out: 200 ms ease-in.
  • Stacking: 12 px gap, 16 px from the bottom edge of the viewport

Implementation

toast.tsx
import { create } from "zustand"

type Tone = "info" | "success" | "destructive"

interface ToastItem {
  id: string
  title: string
  description?: string
  tone: Tone
  duration: number
}

interface ToastStore {
  toasts: ToastItem[]
  push: (t: Omit<ToastItem, "id" | "duration"> & { duration?: number }) => void
  dismiss: (id: string) => void
}

export const useToastStore = create<ToastStore>((set) => ({
  toasts: [],
  push: ({ duration = 4000, ...rest }) => {
    const id = crypto.randomUUID()
    set((s) => ({ toasts: [...s.toasts, { id, duration, ...rest }] }))
    if (duration > 0) {
      setTimeout(() => {
        set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) }))
      }, duration)
    }
  },
  dismiss: (id) => set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })),
}))

export const toast = (opts: Omit<ToastItem, "id" | "duration"> & { duration?: number }) =>
  useToastStore.getState().push(opts)

Props

PropTypeDefaultDescription
titlestringNoneThe headline
descriptionstringNoneOptional secondary line
tone"info" | "success" | "destructive""info"Visual style
durationnumber4000Auto-dismiss in ms; 0 for persistent