FeedbackToast
Toast
A transient confirmation or error message that floats above the page. Used after a form submit, a save, or a destructive action.
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
--borderon three sides, 3 px on the left in the tone color --surface-elevatedbackground withelevation-2shadow- 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
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | None | The headline |
description | string | None | Optional secondary line |
tone | "info" | "success" | "destructive" | "info" | Visual style |
duration | number | 4000 | Auto-dismiss in ms; 0 for persistent |