eglador-ui-react-toast
v1.0.0-alpha.1
Published
Stacked, position-aware toast notifications for React — imperative API + compound subcomponents, headless hook, zero runtime dependencies
Maintainers
Readme
eglador-ui-react-toast
Sonner-style toast notifications for React — imperative toast() API, six variants, six positions, stacking with hover-expand, swipe-to-dismiss, promise pattern, compound subcomponents, and a headless hook. Tailwind CSS v4, zero runtime dependencies.
Features
- Imperative API —
toast()/.success/.error/.warning/.info/.loading/.custom/.promisefrom any component, no provider, no context wiring - Six variants — differentiated by icon shape only, surface stays in zinc tones
- Six positions — top/bottom × left/center/right; per-toast override allowed
- Stacking — older toasts collapse behind, hover the stack to expand
- Swipe-to-dismiss — pointer-driven horizontal drag (mouse + touch)
- Auto-dismiss — configurable duration, paused on hover, paused on window blur
- Promise pattern —
toast.promise(p, { loading, success, error })transitions in place - Update by id — same id replaces the existing toast (loading → success transitions)
- Compound subcomponents —
Toast.Icon/Title/Description/Action/Cancel/CloseButton - Headless hook —
useToaster()exposes the live store for fully custom UIs - Render-prop on
<Toaster>— swap the default toast renderer entirely - Reduced-motion friendly — animations disable under
prefers-reduced-motion: reduce - Accessible —
role="status"for default / info / success / loading,role="alert"for error / warning,aria-liveset automatically - TypeScript-first — generic over promise data, every prop documented inline
- Zero runtime dependencies — only
clsx+tailwind-merge, both pre-bundled
Installation
npm install eglador-ui-react-toastPeer dependencies: react >= 18 · react-dom >= 18 · tailwindcss ^4
Setup
Add the following to your global stylesheet so Tailwind picks up the component classes:
@import "tailwindcss";
@source "../node_modules/eglador-ui-react-toast";The @source path is relative to the CSS file location:
| Framework | CSS file location | Path |
|---|---|---|
| Next.js (App Router) | app/globals.css | ../node_modules/eglador-ui-react-toast |
| Next.js (src/) | src/app/globals.css | ../../node_modules/eglador-ui-react-toast |
| Vite | src/index.css | ../node_modules/eglador-ui-react-toast |
Quick Start
"use client";
import { Toaster, toast } from "eglador-ui-react-toast";
export function App() {
return (
<>
{/* 1. Mount once at the app root */}
<Toaster position="bottom-right" />
{/* 2. Trigger from anywhere */}
<button onClick={() => toast.success("Saved successfully")}>
Save
</button>
</>
);
}API
Exports
| Export | Purpose |
|---|---|
| Toaster | Root component — render once at the app top level |
| toast | Imperative API: (), .success, .error, .warning, .info, .loading, .custom, .promise, .update, .dismiss |
| Toast | Compound wrapper for toast.custom — Toast.Icon / Body / Title / Description / Action / Cancel / CloseButton |
| useToaster() | Headless hook — { toasts, dismiss, update } |
| useToastItemContext() | Read the current toast inside a custom render |
| ensureToastStyles() | Inject keyframes manually (auto-called by <Toaster>) |
<Toaster /> props
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | — | Scope the Toaster — only toasts with a matching toasterId are rendered. Omit for the default catch-all instance. |
| position | "top-left" \| "top-center" \| "top-right" \| "bottom-left" \| "bottom-center" \| "bottom-right" | "bottom-right" | Default position when a toast doesn't override it |
| visibleCount | number | 3 | How many toasts are visible before older ones collapse |
| duration | number | 4000 | Default auto-dismiss in ms (Infinity disables) |
| closable | boolean | false | Always show the close button on every toast |
| expand | boolean | true | Expand collapsed stack on hover |
| gap | number | 8 | Pixel offset between collapsed toasts |
| width | number | 360 | Toast width (px) |
| pauseOnBlur | boolean | true | Pause timers while the window loses focus |
| render | (toast, dismiss) => ReactNode | — | Override the default per-toast rendering |
| className | string | — | Class on each per-position container |
| container | HTMLElement \| null | document.body | Custom portal target |
toast(...) API
| Call | Returns | Description |
|---|---|---|
| toast(title, opts?) | id | Default toast |
| toast.success(title, opts?) | id | ✓ icon |
| toast.error(title, opts?) | id | ✕ icon, role="alert" |
| toast.warning(title, opts?) | id | ⚠ icon, role="alert" |
| toast.info(title, opts?) | id | ℹ icon |
| toast.loading(title, opts?) | id | spinner, duration: Infinity by default |
| toast.custom(render, opts?) | id | Render function returning custom JSX |
| toast.promise(p, msgs, opts?) | id | Loading → success / error transition |
| toast.update(id, patch) | boolean | Update an existing toast |
| toast.dismiss(id?) | void | Dismiss one (or all when id omitted) |
ToastOptions
| Prop | Type | Description |
|---|---|---|
| id | string \| number | Stable id for update / dismiss |
| description | ReactNode | Secondary line below title |
| action | { label, onClick } | Inline call-to-action button |
| cancel | { label, onClick } | Outline cancel button |
| duration | number | Override default auto-dismiss (ms) |
| position | ToastPosition | Override default position for this toast |
| closable | boolean | Force a close button on this toast |
| toasterId | string | Route this toast to a specific <Toaster id="..." /> instance |
| icon | ReactNode | Override the variant icon |
| className / style | — | Per-toast styling overrides |
| onDismiss(id) | (id) => void | Fires whenever the toast leaves |
| onAutoClose(id) | (id) => void | Fires only when the timer dismisses (not user) |
Recipes
All variants
toast("Saved successfully");
toast.success("Profile updated");
toast.error("Couldn't save changes");
toast.warning("Storage almost full");
toast.info("New version available");
toast.loading("Uploading…");Action button (Undo)
toast("Item moved to trash", {
action: { label: "Undo", onClick: () => restore() },
duration: 6000,
});Destructive confirmation
toast.error("Delete this draft?", {
description: "This action cannot be undone.",
action: { label: "Delete", onClick: () => deleteDraft() },
cancel: { label: "Cancel", onClick: () => {} },
duration: Infinity,
});Promise pattern
toast.promise(saveDocument(), {
loading: "Saving…",
success: (data) => `Saved ${data.filename}`,
error: (err) => `Failed: ${err.message}`,
});Update by id
const id = toast.loading("Uploading…", { duration: Infinity });
await upload();
toast.success("Done!", { id, duration: 3000 });Custom compound layout
import { Toast, toast } from "eglador-ui-react-toast";
toast.custom((t) => (
<Toast toast={t} dismiss={() => toast.dismiss(t.id)}>
<Toast.Body>
<Toast.Title>New message</Toast.Title>
<Toast.Description>"Can we ship this on Friday?"</Toast.Description>
</Toast.Body>
<Toast.Action onClick={() => openReply()}>Reply</Toast.Action>
<Toast.CloseButton />
</Toast>
));Free-form custom JSX
toast.custom(() => (
<div className="w-full p-3 rounded-sm border border-zinc-200 bg-zinc-900 text-white">
Anything you want — no row chrome.
</div>
));Headless hook
import { useToaster } from "eglador-ui-react-toast";
function NotificationCenter() {
const { toasts, dismiss } = useToaster();
return (
<ul>
{toasts.map((t) => (
<li key={t.id}>
{t.title} <button onClick={() => dismiss(t.id)}>×</button>
</li>
))}
</ul>
);
}Render-prop on <Toaster>
<Toaster
render={(t, dismiss) => (
<div className="my-toast-shell">
<span>{t.title}</span>
<button onClick={dismiss}>×</button>
</div>
)}
/>Persistent toast
toast("Persistent message", {
duration: Infinity,
closable: true,
});Scoped Toasters (multi-instance)
Mount more than one <Toaster> and route specific toasts to specific instances by id:
// Two scoped toasters — e.g. one inside a modal, one global:
<Toaster id="modal" position="top-center" />
<Toaster position="bottom-right" /> {/* default catch-all */}
toast.success("Saved"); // → bottom-right
toast.error("Form invalid", { toasterId: "modal" }); // → top-centerThe default <Toaster> (no id) catches every toast that doesn't supply a toasterId. Toasters with an id only render toasts whose toasterId matches.
Compatibility
Works with any React-based framework: Next.js, Remix, Vite + React, Gatsby.
<Toaster /> is marked "use client" (it uses useState / useEffect and the DOM). Place it inside a client component / after a "use client" directive. The store itself is module-level — call toast(...) from event handlers and effects, not server components.
Development
npm install
npm run dev # tsup watch mode
npm run build # production build to dist/
npm run typecheck # tsc --noEmit
npm run storybook # Storybook dev (http://localhost:6006)
npm run build-storybook # static Storybook exportPublishing
Publishing is automated via GitHub Actions. When a GitHub Release is created, the package is published to npm.
- Update
versioninpackage.json - Commit and push
- Create a GitHub Release with a matching tag (e.g.
v1.0.0)
Author
Kenan Gündoğan — https://github.com/kenangundogan
Maintained under Eglador
License
MIT
