Demo: 16:9 transcript GIF of the toasters in action
Look, if you've built literally anything that's even remotely "vibe coded" in the past year, you're probably using Radix UI. Their toast component is solid, but there are some really annoying problems. For example, the second you want your notifications to slide in from literally anywhere other than top-right, or you want them to fade instead of slide, or god forbid you want a progress bar that doesn't look like it's from the 2000s... you're basically telling it to rewrite the whole thing.
I got tired of fighting with Radix's defaults every single project, so here's a dead simple wrapper that just works. Six position options, three animation styles, automatic slide directions, and a gradient progress bar.
Every toast library either:
- Gives you near-to-no control (looking at you, default Radix setup)
- Makes you install 27 dependencies and configure a build system
- Assumes you want exactly one specific aesthetic that definitely isn't yours
This is different. It's a tiny layer on top of Radix that lets you:
- Change position globally (
top-right,top-left,bottom-right,bottom-left,top-center,bottom-center) - Pick your animation (
slide,fade,scale) - Override per-toast if you need to
- Get a progress bar with gradient support out of the box
- Have slide directions that automatically match the position (because why would a bottom toast slide from the top?)
No Tailwind required. No weird build config. Just copy the folder and go.
npm install @radix-ui/react-toast1. Copy the src/better-toast/ folder into your project
2. Import the CSS once (probably in your root layout):
import "@/better-toast/toast-animations.css";3. Mount the provider (in app/layout.tsx or wherever your app starts):
import { BetterToastProvider } from "@/better-toast";
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<BetterToastProvider
position="top-right"
animation="slide"
duration={5000}
progressColors={["#1fa0c9", "#28b0e2", "#5fd3fa"]}
animationDuration={220}
animationEasing="cubic-bezier(0.16, 1, 0.3, 1)"
/>
</body>
</html>
);
}4. Use it anywhere:
import { useBetterToast } from "@/better-toast";
function YourComponent() {
const { addToast } = useBetterToast();
return (
<button
onClick={() =>
addToast({
title: "Changes saved",
description: "Your edits are live.",
duration: 3000, // optional override
})
}
>
Save
</button>
);
}The slide animations actually follow the position. If you put toasts in bottom-left, they slide in from the left and swipe dismiss to the left. top-center slides down from the top. It's obvious in hindsight but somehow nobody does this by default.
Global settings (on the provider):
position- where toasts appearanimation- how they appear (slide,fade,scale)duration- how long they stick around (ms)progressColors- array of 1-3 colors for the baranimationDuration- speed of enter/exit (ms)animationEasing- CSS timing function
Per-toast overrides:
addToast({
title: "...",
description: "...",
duration: 3000,
positionOverride: "bottom-center",
animationOverride: "fade",
action: <button>Undo</button>, // optional
variant: "destructive", // or "default"
});type ToastPosition =
| "top-right" | "top-left"
| "bottom-right" | "bottom-left"
| "top-center" | "bottom-center";
type ToastAnimation = "slide" | "fade" | "scale";
// Hook returns:
const { toasts, addToast, dismiss, dismissAll } = useBetterToast();Base styles are inline for portability. Animations live in toast-animations.css. Change whatever you want - it's your project.
This is meant to be copied into your project, not installed from npm (though you could package it if you want). The whole point is you own the code and can tweak it.