React-Hot-Toast: The Complete Guide to Toast Notifications







React
Notifications
Frontend

React-Hot-Toast: The Only Toast Notification Guide You’ll Actually Finish

A production-ready walkthrough of
react-hot-toast
from zero to a fully customized, promise-aware
React notification system in minutes, not hours.

Why React-Hot-Toast Won the Toast Wars

Every React project eventually needs to tell the user something: a form submitted successfully,
an API call failed, a file is uploading. The naive solution — a local useState flag
and a hand-rolled <div> — works until it doesn’t, which is usually around the
time you need the same notification from three different components simultaneously.
That’s when developers reach for a dedicated React toast notification library,
and increasingly, that library is
react-hot-toast.

What separates react-hot-toast from the crowded field of
React notification libraries is an almost aggressively minimal API surface.
You get one hook (useToaster), one imperative function (toast()),
one component (<Toaster />), and exactly the features you actually use —
nothing more. The bundle weighs in at under 5 kB gzipped. For comparison,
react-toastify
ships at roughly 3× that size, and
notistack
drags in a full Material-UI dependency if you’re not careful.
When bundle size is a first-class concern — and it should be — react-hot-toast wins by
not bringing anything to the party it wasn’t explicitly invited to.

The library is also genuinely headless-first in practice. Its default styles look
clean out of the box, but they’re treated as a starting point rather than a constraint.
You can replace every visual element with arbitrary JSX using toast.custom(),
plug in Tailwind classes, or integrate with your design system’s token layer — without
monkey-patching CSS or wrestling with specificity wars. That design decision alone explains
why react-hot-toast has become the go-to
React toast library for teams that care about visual consistency.

Installation and Initial Setup

React-hot-toast installation follows the standard npm/yarn pattern and has no
mandatory peer dependencies beyond React itself (version 16.8+ for hooks support).
Open your terminal and run one of the following:

# npm
npm install react-hot-toast

# yarn
yarn add react-hot-toast

# pnpm
pnpm add react-hot-toast

Once installed, the react-hot-toast setup requires exactly one configuration
step: mounting the <Toaster /> component somewhere high in your component
tree — typically in App.jsx, your root layout, or a Next.js
_app.tsx. This component is the rendering engine. It listens to an internal
store and paints notifications into the DOM. Without it, toast() calls are
queued silently — nothing breaks, but nothing appears either, which makes for a confusing
first ten minutes if you skip this step.

// App.jsx
import { Toaster } from 'react-hot-toast';

export default function App() {
  return (
    <>
      <Toaster
        position="top-right"
        reverseOrder={false}
        gutter={8}
        toastOptions={{
          duration: 4000,
          style: {
            background: '#1e1e2e',
            color: '#cdd6f4',
          },
        }}
      />
      {/* rest of your app */}
    </>
  );
}

With <Toaster /> in place, you can fire React toast messages
from absolutely anywhere — event handlers, async functions, third-party callbacks, even
outside React’s component tree entirely (useful for Axios interceptors or Redux middleware).
Call toast('Message') and the notification appears. That’s the entire
getting-started surface. Everything beyond this is refinement.

Next.js note: In the App Router (Next.js 13+), place
<Toaster /> inside a Client Component wrapper and import it into your
root layout.tsx. The library uses browser APIs internally, so it cannot
be rendered in a Server Component.

The Core API: More Than Just toast()

The base toast() function accepts a string, a JSX element, or a render
function — giving you a React toast notification with a single line.
But the typed variants are where the ergonomics really shine.
toast.success(), toast.error(), and toast.loading()
each apply appropriate icons, colors, and ARIA roles automatically, which matters both
for aesthetics and for users relying on assistive technology.

import toast from 'react-hot-toast';

// Basic variants
toast('Default message');
toast.success('Profile saved successfully!');
toast.error('Something went wrong — please retry.');
toast.loading('Uploading your file...');

// Programmatic dismissal
const toastId = toast.loading('Processing...');
// later:
toast.dismiss(toastId);

// Dismiss all
toast.dismiss();

Each call returns a unique toastId string. You can use this ID to update an
existing notification in-place rather than dismissing it and firing a new one — a subtle
but important UX detail. Rapidly replacing toasts causes visual flicker and cognitive noise.
Updating in-place keeps the UI calm while the underlying state changes. Pass the ID to a
subsequent toast() call via the id option:
toast.success('Done!', { id: toastId }).

The library also exposes React toast hooks for when you need direct access
to the notification queue. useToaster() returns the current list of active toasts
and the event handlers required to build a fully custom renderer — this is the entry point
for genuinely headless implementations where <Toaster /> is never used at all.
useToast() (internal) manages the store. These hooks follow React’s composition
model cleanly and integrate with state management libraries without friction.

The Promise API: Async Notifications Without the Boilerplate

If react-hot-toast has a signature feature, it’s toast.promise(). The pattern
it replaces looks like this in the wild: a try/catch block, three separate
toast() calls, manual ID tracking for the loading state, and a
finally clause to clean up. It’s not complex, but it’s repetitive, and
repetitive code is where bugs nest. toast.promise() collapses all of that
into a single declarative call.

import toast from 'react-hot-toast';
import { saveUserProfile } from './api';

async function handleSave(data) {
  await toast.promise(
    saveUserProfile(data),
    {
      loading: 'Saving your profile...',
      success: (result) => `Welcome back, ${result.name}!`,
      error: (err) => `Save failed: ${err.message}`,
    }
  );
}

Notice that both success and error accept either a static string
or a function that receives the resolved value or rejection reason respectively. This means
you can surface server-returned error messages — or personalized success text — without any
additional state management. The react-hot-toast promise handler transitions
the notification through loading → success/error automatically, applying the correct icon
and timing at each stage.

A practical note worth saving you a debugging session: toast.promise() does
not suppress the rejection. If your Promise rejects, the error toast fires
and the rejection propagates normally. This means wrapping the call in a
try/catch or attaching a .catch() handler is still appropriate
when you need to handle the error beyond displaying a notification — for example, to
reset form state or log to an error reporting service like
Sentry.

Customization: From Default to Pixel-Perfect

React-hot-toast customization operates at three distinct levels, and
understanding which level to reach for saves a significant amount of unnecessary complexity.
At the broadest level, <Toaster /> accepts a toastOptions
prop that applies defaults to every notification the library renders. This is where you
set brand fonts, border-radius, background color, shadow, and default duration — effectively
theming the entire notification system in one place.

<Toaster
  toastOptions={{
    className: '',
    style: {
      border: '1px solid #7c3aed',
      padding: '12px 16px',
      borderRadius: '10px',
      color: '#1a1a2e',
      fontFamily: 'Inter, sans-serif',
    },
    success: {
      iconTheme: {
        primary: '#7c3aed',
        secondary: '#ffffff',
      },
    },
  }}
/>

At the individual toast level, you can pass a style object or
className directly to any toast() call, overriding the global
defaults for that specific notification. This is the right tool for one-off cases: a
critical error with a red border, a success message with a confetti icon, a warning that
stays visible longer than usual. Mix global defaults with local overrides and you get a
consistent system that still allows exceptions where the design demands them.

For complete visual control — including custom layout, animations, and interactive elements
toast.custom() lets you pass arbitrary JSX as the notification body.
Combined with Tailwind CSS,
this becomes the standard pattern for teams building design-system-native
React alert notifications. You retain react-hot-toast’s queueing,
deduplication, and accessibility infrastructure while rendering whatever UI your
system requires:

toast.custom((t) => (
  <div
    className={`
      flex items-center gap-3 px-4 py-3 rounded-xl shadow-lg
      bg-white border border-violet-200
      ${t.visible ? 'animate-enter' : 'animate-leave'}
    `}
  >
    <span className="text-2xl">🎉</span>
    <div>
      <p className="font-semibold text-gray-900">Subscription activated!</p>
      <p className="text-sm text-gray-500">Your 14-day trial starts now.</p>
    </div>
    <button onClick={() => toast.dismiss(t.id)} className="ml-auto text-gray-400">✕</button>
  </div>
), { duration: 6000 });

Advanced Patterns for Production React Notification Systems

At scale, calling toast() directly from every component that needs a
notification creates the same coupling problem that local state does — just at a higher
level. The production pattern is to abstract your notification calls into a thin utility
module that wraps react-hot-toast and enforces your team’s conventions: maximum duration,
approved variants, required copy review for error messages, analytics event firing.
This wrapper becomes the single source of truth for how your application communicates
with users.

// lib/notify.js
import toast from 'react-hot-toast';
import { trackEvent } from './analytics';

export const notify = {
  success: (msg, opts) => {
    trackEvent('notification_shown', { type: 'success' });
    return toast.success(msg, { duration: 3000, ...opts });
  },
  error: (msg, opts) => {
    trackEvent('notification_shown', { type: 'error' });
    return toast.error(msg, { duration: 6000, ...opts });
  },
  promise: (promise, messages, opts) =>
    toast.promise(promise, messages, opts),
};

// Usage anywhere in the app
import { notify } from '@/lib/notify';
notify.success('Changes saved.');

Deduplication is another production concern. If a user hammers a submit button while
a network request is in-flight, you don’t want five identical loading toasts stacking up.
React-hot-toast supports explicit deduplication via the id option — if you
fire toast.loading('Saving...', { id: 'save-op' }) multiple times,
only one notification appears. The subsequent calls silently update the existing one.
Combining this with your wrapper utility means deduplication becomes policy, not something
each developer has to remember individually.

For applications with significant accessibility requirements — healthcare, finance,
government — it’s worth auditing how your React notification system handles
ARIA live regions. React-hot-toast applies role="status" for success/default
toasts and role="alert" for error toasts by default, which covers the majority
of cases. If your notifications contain interactive elements (buttons, links), ensure
they’re reachable via keyboard navigation and that focus management after dismissal is
handled explicitly. The toast.custom() path puts this responsibility in
your hands — which is appropriate, since the library can’t make semantic decisions about
your custom JSX.

React-Hot-Toast vs. the Alternatives: An Honest Take

The two most common alternatives developers evaluate alongside react-hot-toast are
react-toastify
and the newer Sonner.
React-toastify is the veteran — it’s been the default answer to “how do I do
React toast messages” for years, and it shows. The API has accumulated
features across major versions in ways that occasionally feel inconsistent. Its bundle size
is larger, its CSS is more opinionated, and overriding that CSS in a design system
context requires more effort. It’s a solid library, but “solid and feature-rich” and
“minimal and composable” are genuinely different tradeoffs, not a quality gradient.

Sonner — by Emil Kowal — is the newest serious contender. It takes inspiration directly
from react-hot-toast’s API philosophy but adds stacked toast UI (the layered cards visible
as notifications stack), first-class Radix UI integration, and improved animation primitives.
If you’re building with shadcn/ui or a Radix-based design system, Sonner’s integration
story is currently stronger. If you’re building a custom design system from scratch or
need maximum control with minimum dependencies, react-hot-toast’s headless path via
useToaster() remains the more flexible choice.

The honest recommendation: react-hot-toast is the right default for most React projects.
Its learning curve is measured in minutes, its bundle cost is negligible, and its API
will not surprise you at 11pm the night before a release. Reach for Sonner if your
design requires stacked toast UI natively. Reach for react-toastify only if you’re
inheriting a codebase that already uses it. Starting fresh with react-toastify in 2024
is a decision that mostly benefits whoever writes the migration ticket later.

A Real-World React-Hot-Toast Example: Form Submission Flow

To make this concrete, here’s a complete example: a contact form that uses
react-hot-toast to communicate every meaningful state change to the user —
validation errors, loading state, server success, and network failure.
This is closer to what production usage looks like than the minimal snippets
in most documentation.

import { useState } from 'react';
import toast from 'react-hot-toast';
import { submitContactForm } from './api';

export function ContactForm() {
  const [isSubmitting, setIsSubmitting] = useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    const data = Object.fromEntries(new FormData(e.target));

    if (!data.email.includes('@')) {
      toast.error('Please enter a valid email address.', {
        id: 'email-validation',
      });
      return;
    }

    setIsSubmitting(true);

    try {
      await toast.promise(submitContactForm(data), {
        loading: 'Sending your message...',
        success: "Message sent! We'll get back to you within 24 hours.",
        error: (err) =>
          err.status === 429
            ? 'Too many requests — please wait a moment.'
            : 'Failed to send. Please try again.',
      });
      e.target.reset();
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" placeholder="your@email.com" required />
      <textarea name="message" placeholder="Your message..." required />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Sending...' : 'Send Message'}
      </button>
    </form>
  );
}

A few things worth noting in this pattern. The validation error uses a fixed
id: 'email-validation' — so if the user submits three times with a bad email,
they see exactly one toast, not three. The finally block resets
isSubmitting regardless of outcome, preventing the button from staying
disabled after an error. And e.target.reset() only runs on the success
path, because resetting a form after a failed submission is almost always wrong UX.
These are small details, but they’re the kind that separate implementations users
trust from implementations users complain about.

This example also illustrates why react-hot-toast’s error handler receives the rejection
value directly — the err.status === 429 check gives users a meaningfully
different message for a rate-limit error versus a generic server failure.
Generic error messages are a failure mode disguised as a design decision. React-hot-toast
gives you the tools to do better; whether you use them is a product quality choice, not
a library limitation.

FAQ

How do I install and set up react-hot-toast?

Run npm install react-hot-toast (or the yarn/pnpm equivalent) in your
project root. Then add <Toaster /> from 'react-hot-toast'
to your app’s root component — App.jsx, _app.tsx, or a
root layout file. After that, import toast and call it from any component
or async function. The entire setup takes under two minutes and requires no additional
configuration to produce working, styled notifications.

How do I customize toast styles in react-hot-toast?

There are three customization paths. For global defaults, pass a toastOptions
prop to <Toaster /> with a style object or
className. For per-notification overrides, pass style or
className directly to any toast() call. For full visual
control — custom layout, animations, interactive elements — use toast.custom()
and return any JSX. All three approaches can be combined: global defaults for consistency,
local overrides for exceptions, toast.custom() for design-system-native
components.

How does the toast.promise() API work in react-hot-toast?

toast.promise(yourPromise, messages) accepts any Promise and a messages
object with loading, success, and error keys.
While the Promise is pending, a loading toast appears. When it resolves, the toast
transitions to a success state. When it rejects, it transitions to an error state.
Both success and error accept functions that receive the
resolved/rejected value, letting you display dynamic content. The Promise’s outcome
still propagates normally — toast.promise() observes the Promise, it
doesn’t swallow it.

Further Reading