TL;DR: React Hot Toast is a zero-dependency toast library with a hook-based API. Call
toast('message')ortoast.promise(fn, {loading, success, error})from anywhere in your app. Renders via portal at the root. ~5kB gzipped. Official docs: react-hot-toast.com.
What is React Hot Toast?
React Hot Toast is a lightweight, zero-dependency toast notification library. It uses a hook-based API and renders toasts in a portal at the top level. The toast.promise() helper is particularly handy for async operations - it handles loading, success, and error states in one call.
The design choice that makes it pleasant is that toast() is callable from anywhere, not just inside a component. You import the function, call it in an event handler, a data-fetching utility, or a Redux thunk, and the message appears. There's no context to thread, no prop to pass down, no ref to wire up. You drop one <Toaster /> at the root and forget about it. Everything else is a plain function call.
That portability is why it spread so widely. Form submissions, API errors, copy-to-clipboard confirmations, and background sync results all need a quick "here's what happened" message, and they often fire from code that's nowhere near a component. A global toast function fits that reality better than a component you have to render in the right place.
Picking it over the alternatives
React Hot Toast is a solid, well-tested choice with a large community. It's slightly older than Sonner and has more manual customisation overhead for advanced styling, but it's battle-tested and widely deployed. If you want a library that's been hammered on in production for years and has answers to nearly every edge case on Stack Overflow, this is a safe pick.
How does it compare to Sonner? Both are excellent. Sonner has slicker stacking animations and newer design defaults, and it's the toast component shadcn/ui ships now. React Hot Toast has a larger install base, more community resources, and a headless mode that gives you total rendering control. My rule of thumb: reach for Sonner when you want gorgeous defaults with little effort, and React Hot Toast when you want a stable base you'll style heavily yourself. Neither is a wrong answer, and migrating between them later is a small job.
One thing to keep in mind regardless of choice: toasts are for transient, non-critical feedback. A failed payment deserves a persistent error in the form, not a message that vanishes in four seconds. Use toasts for confirmations and soft warnings, not for anything the user must act on.
Key Features
toast.promise()- wraps async operations with loading/success/error states- Custom renderers - pass JSX to
toast.custom()for fully custom toast UI - Headless mode - use
useToaster()hook to build your own toast UI - Pause on hover - toasts pause countdown on mouse enter
- Accessible - ARIA live regions for screen readers
Installation
npm install react-hot-toast
Setup
import { Toaster } from 'react-hot-toast'
function App() {
return (
<>
<YourApp />
<Toaster
position="top-right"
toastOptions={{
duration: 4000,
style: { background: '#0d0e1a', color: '#f1f5f9' },
}}
/>
</>
)
}
Basic Usage
import toast from 'react-hot-toast'
// Simple
toast('Message sent')
toast.success('Saved!')
toast.error('Failed to save')
// Promise
async function handleSubmit() {
await toast.promise(
submitForm(data),
{
loading: 'Submitting...',
success: 'Form submitted!',
error: (err) => `Error: ${err.message}`,
}
)
}
Custom Toast
toast.custom((t) => (
<div className={`flex items-center gap-3 bg-white shadow-lg rounded-lg p-4 ${
t.visible ? 'animate-enter' : 'animate-leave'
}`}>
<CheckCircle className="text-green-500" size={20} />
<div>
<p className="font-medium">Upload complete</p>
<p className="text-sm text-slate-500">Your file has been uploaded</p>
</div>
<button onClick={() => toast.dismiss(t.id)} className="ml-auto text-slate-400">X</button>
</div>
))
TypeScript Tips
Use the headless useToaster hook for full type control:
import { useToaster } from 'react-hot-toast'
function ToastContainer() {
const { toasts, handlers } = useToaster()
const { startPause, endPause, calculateOffset, updateHeight } = handlers
return (
<div onMouseEnter={startPause} onMouseLeave={endPause}>
{toasts.map(t => {
const offset = calculateOffset(t, { reverseOrder: false })
const ref = (el: HTMLDivElement) => el && updateHeight(t.id, el.getBoundingClientRect().height)
return (
<div key={t.id} ref={ref} style={{ transform: `translateY(${offset}px)` }}>
{/* render your toast */}
</div>
)
})}
</div>
)
}
Accessibility and timing details
Toasts announce themselves through an ARIA live region, so screen reader users hear the message without losing their place. That's the part most hand-rolled notification systems skip, and it's the reason a library is worth using here at all. Keep your messages short and meaningful, because a live region reads the whole thing aloud and a wall of text is hard to follow by ear.
Timing deserves thought too. The default four-second duration is fine for a short confirmation, but it's too quick for a longer error message someone needs to read and digest. Bump the duration for error toasts, or set it to Infinity and let the user dismiss the message themselves. Pause-on-hover helps as well, since it gives anyone who's reading a moment to finish before the toast slides away.
Common Gotchas
- Render
<Toaster />once - at the app root; duplicates cause conflicting animations and stacked, misplaced toasts. toast.dismiss(id)- programmatically dismiss a specific toast; call without an ID to dismiss all of them at once.- Custom styles don't hot-reload -
toastOptions.styleset at<Toaster>level doesn't always pick up HMR changes; restart the dev server if styles seem stuck. - Deduplicate with an id - pass an
idtotoast()to update an existing toast instead of stacking a new one, which is handy for progress updates on the same action.
Related
- Sonner - the newer toast alternative with better stacking animations; the default in shadcn/ui
- React 19 Guide - use toast notifications to surface results from React 19 Actions and
useActionState