@subrotosaha/notification
v1.2.0
Published
A lightweight toast notification and confirmation dialog library for any JavaScript framework
Maintainers
Readme
@subrotosaha/notification
A lightweight, zero-dependency toast notification & confirm dialog library for vanilla JS, React, Vue, Angular, and Svelte. Features multiple visual variants, six positions, inline action buttons, promise tracking, 15 input types, async hooks, multi-step dialogs, 32 built-in icons, custom icon registration, icon library integration (Lucide, Iconify, etc.), and full TypeScript support.
Table of Contents
Installation
npm install @subrotosaha/notificationSetup
Vanilla JS
import notification from "@subrotosaha/notification";
import "@subrotosaha/notification/style.css";React
// main.jsx or App.jsx — import styles once at the root
import "@subrotosaha/notification/style.css";
// In any component
import notification from "@subrotosaha/notification";Note: This library manipulates
document.bodydirectly — it is client-side only. In Next.js or other SSR environments, call it insideuseEffector a browser-only code path.
Next.js (App Router)
"use client";
import notification from "@subrotosaha/notification";
import "@subrotosaha/notification/style.css";
export default function Page() {
const handleClick = () => {
notification.notify.success("Saved successfully!", {
position: "top-right",
});
};
return <button onClick={handleClick}>Save</button>;
}Global Config
Set default options applied to all subsequent toasts and/or confirm dialogs. Call once during app bootstrap — options are merged, not replaced.
notification.config({
toast: {
position: "bottom-right",
duration: 4000,
variant: "filled",
pauseOnHover: true,
progressBar: true,
},
confirm: {
confirmButtonText: "Yes",
cancelButtonText: "No",
},
});| Field | Type | Description |
| --------- | ---------------------------- | ----------------------------------------------- |
| toast | Partial<GlobalToastConfig> | Default position, duration, variant, etc. |
| confirm | Partial<FireOptions> | Default confirm dialog options |
Toast Notifications
Basic Usage
notification.notify.success("Profile saved!");
notification.notify.warning("Low disk space.");
notification.notify.error("Upload failed.");
notification.notify.info("Your session expires soon.");Each method returns a numeric toast ID (number | null).
notification.notify.success("File uploaded", {
title: "Upload Complete",
variant: "filled",
position: "bottom-right",
duration: 3000,
progressBar: true,
});Loading Toast
Displays a persistent spinner toast that never auto-dismisses. Call dismiss(id) when done.
const id = notification.notify.loading("Uploading…");
await upload();
notification.notify.dismiss(id);Promise Toast
Tracks an async operation and transitions through loading → success | error automatically.
notification.notify.promise(
fetch("/api/save").then((r) => r.json()),
{
loading: "Saving…",
success: (data) => `Saved ${data.count} records.`,
error: (err) => `Failed: ${err.message}`,
}
);The original promise is returned so it can still be awaited:
const data = await notification.notify.promise(fetchUser(id), {
loading: "Loading user…",
success: "User loaded!",
error: "Could not load user.",
});Dismiss
// Dismiss a single toast by ID
notification.notify.dismiss(id);
// Dismiss all visible toasts
notification.notify.dismissAll();NotifyOptions
All fields are optional and merge with global defaults from config().
| Option | Type | Default | Description |
| -------------- | ---------------------------------- | ------------- | --------------------------------------------------------------------------- |
| title | string \| null | null | Optional bold title above the message |
| variant | 'filled' \| 'outlined' | '' | Visual style |
| position | NotifyPosition | 'top-right' | Screen position |
| duration | number | 5000 | Auto-dismiss delay in ms. 0 = persistent |
| pauseOnHover | boolean | true | Pause the timer while the cursor is over the toast |
| progressBar | boolean | false | Show an animated draining progress bar |
| action | NotifyActionConfig \| null | null | Inline action button (e.g. "Undo") |
| onClick | () => void \| null | null | Called when the toast body is clicked (then dismisses) |
| onClose | (reason: string) => void \| null | null | Called after the toast is removed from the DOM |
| iconHtml | string \| Element \| null | null | Custom HTML string, SVG string, or DOM Element replacing the default icon |
| closable | boolean | true | Show or hide the × close button |
Action Button
Render an inline action button on the right side of the toast.
notification.notify.warning("Item moved to trash.", {
action: {
text: "Undo",
onClick: () => restoreItem(),
},
});Positions
| Value | Description |
| ----------------- | ----------------------------------- |
| 'top-right' | Top-right corner (default) |
| 'top-left' | Top-left corner |
| 'top-center' | Top-center, slides down from above |
| 'bottom-right' | Bottom-right corner |
| 'bottom-left' | Bottom-left corner |
| 'bottom-center' | Bottom-center, slides up from below |
Toasts at the same position stack and reposition automatically when one is dismissed.
Toast Variants
| Variant | Description |
| ------------ | ---------------------------------------------------- |
| (default) | Tinted glassmorphism background with coloured border |
| 'filled' | Solid saturated background with white text |
| 'outlined' | Transparent background with a 1.5 px coloured border |
Confirm Dialogs
fire()
Full options API — returns Promise<FireResult<T>>.
const result = await notification.confirm.fire({
icon: "warning",
title: "Delete account?",
text: "All your data will be permanently removed.",
confirmButtonText: "Yes, delete it",
cancelButtonText: "Cancel",
showDenyButton: true,
denyButtonText: "Archive instead",
});
if (result.isConfirmed) {
// confirm button clicked
} else if (result.isDenied) {
// deny button clicked
} else if (result.isDismissed) {
console.log("Dismissed by:", result.dismiss); // 'cancel' | 'backdrop' | 'esc' | 'close' | 'timer'
}Shorthand Methods
Pre-configured fire() calls with a specific icon. Accept a FireOptions object (minus icon).
notification.confirm.success(options?)
notification.confirm.warning(options?)
notification.confirm.error(options?)
notification.confirm.info(options?)
notification.confirm.question(options?)Example:
const result = await notification.confirm.warning({
title: "Delete item?",
text: "This cannot be undone.",
confirmButtonText: "Delete",
});
if (result.isConfirmed) {
await deleteItem();
notification.notify.success("Deleted.", { variant: "filled" });
}// No cancel button
await notification.confirm.info({
title: "Heads up",
text: "Your free trial ends in 3 days.",
showCancelButton: false,
confirmButtonText: "Got it",
});mixin()
Create a reusable factory with shared defaults. Per-call options override the mixin defaults.
const DeleteAlert = notification.confirm.mixin({
icon: "warning",
confirmButtonText: "Delete",
cancelButtonText: "Cancel",
showCancelButton: true,
});
const result = await DeleteAlert.fire({ title: "Delete this record?" });
const result2 = await DeleteAlert.fire({ title: "Delete all records?" });queue()
Show multiple dialogs in sequence. Stops early if any step is dismissed.
const steps = ["1", "2", "3"];
const { results, dismiss, isConfirmed } = await notification.confirm.queue([
{
title: "Step 1 — Enter your name",
input: "text",
inputLabel: "Name",
progressSteps: steps,
currentProgressStep: 0,
},
{
title: "Step 2 — Enter your email",
input: "email",
inputLabel: "Email",
progressSteps: steps,
currentProgressStep: 1,
},
{
title: "Step 3 — Confirm",
text: "Review your details and submit.",
progressSteps: steps,
currentProgressStep: 2,
},
]);
if (isConfirmed) {
console.log(
"Values:",
results.map((r) => r.value)
);
}Note:
queue()auto-injectscurrentProgressStepandprogressStepswhen not provided, numbering steps"1","2","3", etc.
Runtime Controls
// Check if a dialog is currently visible
notification.confirm.isVisible(); // boolean
// Check if preConfirm is running
notification.confirm.isLoading(); // boolean
// Get the popup DOM element
const popup = notification.confirm.getPopup(); // HTMLElement | null
// Programmatically close with 'close' dismiss reason
notification.confirm.close();
// Update live dialog options while it is open
notification.confirm.update({
title: "Still processing…",
confirmButtonText: "Please wait",
showCancelButton: false,
});FireOptions Reference
| Option | Type | Default | Description |
| --------------------- | -------------------------------------------------------- | ------------ | ------------------------------------------------------------------- |
| title | string | '' | Dialog title (HTML allowed) |
| text | string | '' | Plain-text body content |
| html | string | '' | HTML body content (overrides text) |
| icon | IconType (built-in name or any registered custom name) | 'question' | Icon to display — see Icons for all 32 built-in names |
| iconHtml | string \| Element \| null | null | Custom HTML string or DOM Element that replaces the icon entirely |
| variant | 'filled' \| 'outlined' | '' | Visual style |
| showConfirmButton | boolean | true | Show the Confirm button |
| showDenyButton | boolean | false | Show an optional Deny button |
| showCancelButton | boolean | true | Show the Cancel button |
| confirmButtonText | string | 'OK' | Confirm button label |
| denyButtonText | string | 'No' | Deny button label |
| cancelButtonText | string | 'Cancel' | Cancel button label |
| reverseButtons | boolean | false | Swap Confirm and Cancel button order |
| showCloseButton | boolean | false | Show an × close button in the corner |
| allowOutsideClick | boolean \| () => boolean | true | Close on backdrop click |
| allowEscapeKey | boolean \| () => boolean | true | Close on Escape key |
| footer | string | '' | HTML/text rendered below the action buttons |
| imageUrl | string | '' | URL for a custom image inside the dialog |
| imageWidth | number \| string \| null | null | CSS width of the custom image |
| imageHeight | number \| string \| null | null | CSS height of the custom image |
| imageAlt | string | '' | Alt text for the custom image |
| input | InputType \| null | null | Render an input field inside the dialog |
| inputLabel | string | '' | Label text above the input |
| inputPlaceholder | string | '' | Input placeholder |
| inputValue | string \| number \| boolean | '' | Pre-filled input value |
| inputOptions | Record<string, string> \| Map<string, string> | {} | Options for select or radio inputs |
| inputAttributes | Record<string, string> | {} | Extra HTML attributes on the input element |
| inputValidator | (value) => string \| null \| Promise<...> | null | Return an error string to block confirm, falsy to allow |
| timer | number \| null | null | Auto-close after N milliseconds |
| timerProgressBar | boolean | false | Show a draining progress bar when timer is set |
| preConfirm | (inputValue) => Promise<T \| false> | null | Async hook on Confirm — return false to keep the dialog open |
| showLoaderOnConfirm | boolean | false | Show a spinner on the Confirm button while preConfirm runs |
| preDeny | () => Promise<false \| void> | null | Async hook on Deny — return false to abort the denial |
| progressSteps | Array<string \| number> | [] | Multi-step progress indicator labels |
| currentProgressStep | number \| null | null | Zero-based index of the active step |
| willOpen | (popup: HTMLElement) => void | null | Called before the dialog animates in |
| didOpen | (popup: HTMLElement) => void | null | Called after the dialog is fully visible |
| willClose | (popup: HTMLElement) => void | null | Called before the dialog begins closing |
| didClose | () => void | null | Called after the dialog is removed from the DOM |
FireResult Object
| Property | Type | Description |
| ------------- | ------------------- | --------------------------------------------------------------------- |
| isConfirmed | boolean | true when the Confirm button was clicked |
| isDenied | boolean | true when the Deny button was clicked |
| isDismissed | boolean | true when the dialog was closed without confirming |
| dismiss | DismissReasonType | Reason string when isDismissed is true |
| value | T | Input value, preConfirm return value, or true for simple confirms |
ConfirmUpdateOptions
Fields that can be changed on a currently open dialog via notification.confirm.update().
| Field | Type | Description |
| ------------------- | --------- | ------------------------------------- |
| title | string | New title HTML |
| text | string | New plain-text body |
| html | string | New rich HTML body (overrides text) |
| confirmButtonText | string | New Confirm button label |
| cancelButtonText | string | New Cancel button label |
| showConfirmButton | boolean | Show or hide the Confirm button |
| showCancelButton | boolean | Show or hide the Cancel button |
Input Types
Pass any of these to the input option:
'text' · 'email' · 'password' · 'number' · 'tel' · 'url' · 'textarea' · 'select' · 'checkbox' · 'radio' · 'range' · 'date' · 'datetime-local' · 'time' · 'search'
Text input with validation:
const result = await notification.confirm.fire({
title: "Enter your email",
input: "email",
inputPlaceholder: "[email protected]",
inputValidator: (value) => {
if (!value) return "Email is required.";
},
confirmButtonText: "Submit",
});
if (result.isConfirmed) console.log("Email:", result.value);Select input:
await notification.confirm.fire({
title: "Choose a country",
input: "select",
inputOptions: { us: "United States", ca: "Canada", gb: "United Kingdom" },
inputPlaceholder: "Select a country",
});Range input:
await notification.confirm.fire({
title: "Select volume",
input: "range",
inputAttributes: { min: "0", max: "100", step: "5" },
inputValue: 50,
});Dismiss Reasons
When isDismissed is true, result.dismiss will be one of:
| Value | Trigger |
| ------------ | --------------------------------- |
| 'backdrop' | Clicked outside the dialog |
| 'cancel' | Clicked the Cancel button |
| 'close' | Clicked the × close button |
| 'esc' | Pressed the Escape key |
| 'timer' | Auto-closed by the timer option |
Use the DismissReason constant for type-safe comparisons:
import notification, { DismissReason } from "@subrotosaha/notification";
const result = await notification.confirm.fire({ title: "Continue?" });
if (result.dismiss === DismissReason.timer) console.log("Timed out");
if (result.dismiss === DismissReason.cancel) console.log("User cancelled");
// Also accessible on the notification object:
notification.DismissReason.backdrop;
notification.DismissReason.esc;Confirm Variants
| Variant | Description |
| ------------ | ------------------------------------------------------------- |
| (default) | Tinted glassmorphism with accent border per icon type |
| 'filled' | Deep solid saturated background matching the icon colour |
| 'outlined' | Glass background with a full-perimeter 1.5 px coloured border |
// Filled
await notification.confirm.fire({
icon: "error",
title: "Delete permanently?",
variant: "filled",
confirmButtonText: "Delete",
});
// Outlined
await notification.confirm.fire({
icon: "info",
title: "Heads up",
text: "Your free trial ends in 3 days.",
variant: "outlined",
showCancelButton: false,
confirmButtonText: "Got it",
});Advanced Examples
Timer with progress bar
await notification.confirm.fire({
icon: "info",
title: "Session expiring",
text: "You will be logged out automatically.",
timer: 5000,
timerProgressBar: true,
showCancelButton: false,
confirmButtonText: "Stay logged in",
});preConfirm — async validation / server action
const result = await notification.confirm.fire({
title: "Submit form?",
input: "textarea",
inputLabel: "Your message",
inputPlaceholder: "Write something…",
confirmButtonText: "Send",
showLoaderOnConfirm: true,
preConfirm: async (message) => {
const response = await fetch("/api/messages", {
method: "POST",
body: JSON.stringify({ message }),
});
if (!response.ok) return false; // keep dialog open
return await response.json();
},
});
if (result.isConfirmed) {
notification.notify.success("Message sent!", { variant: "filled" });
}preDeny — confirm before discarding
await notification.confirm.fire({
title: "Save changes?",
showDenyButton: true,
denyButtonText: "Discard",
preDeny: async () => {
const check = await notification.confirm.warning({
title: "Really discard?",
confirmButtonText: "Yes, discard",
});
if (!check.isConfirmed) return false; // keep original dialog open
},
});Lifecycle hooks
await notification.confirm.fire({
title: "Loading…",
willOpen: (popup) => console.log("opening", popup),
didOpen: (popup) => {
// start animations, fetch data, focus an element…
},
willClose: (popup) => console.log("closing"),
didClose: () => console.log("removed from DOM"),
});Live update() inside preConfirm
await notification.confirm.fire({
title: "Uploading file",
confirmButtonText: "Upload",
preConfirm: async () => {
notification.confirm.update({ confirmButtonText: "Uploading…" });
await uploadFile();
notification.confirm.update({ confirmButtonText: "Done!" });
},
});Icons
Built-in Icons
32 icons are bundled and ready to use by name. Pass them to the icon option in confirm dialogs, or pull their SVG strings from notification.icons.
| Category | Names |
| ----------- | ----------------------------------------------------- |
| Status | success · warning · error · info · question |
| UI Controls | close · spinner · loading |
| Actions | trash · edit · copy · download · upload |
| Navigation | refresh · search · link · mail · bell |
| Feedback | star · heart · thumbup · thumbdown |
| Identity | user · lock · unlock · settings · pin |
| Misc | fire · shield · calendar · clock · home |
await notification.confirm.fire({ icon: "trash", title: "Delete this?" });
await notification.confirm.fire({ icon: "shield", title: "Access restricted" });
await notification.confirm.fire({
icon: "calendar",
title: "Schedule a meeting",
});Custom Icons
Register your own icons with registerIcon(). They work anywhere the icon option is accepted and can override built-ins.
import notification, { registerIcon } from "@subrotosaha/notification";
// Plain SVG string
registerIcon("bitcoin", '<svg viewBox="0 0 320 512">…</svg>');
// Also available on the notification object
notification.registerIcon("bitcoin", '<svg viewBox="0 0 320 512">…</svg>');
// Use by name in confirm dialogs
await notification.confirm.fire({
icon: "bitcoin",
title: "Pay with Bitcoin?",
});
// Or grab the SVG string directly
notification.notify.success("Payment received", {
iconHtml: notification.icons["bitcoin"],
});Access the full live registry at any time:
console.log(notification.icons); // Record<string, string> — built-ins + your custom iconsIcon Library Integration
iconHtml accepts a raw HTML/SVG string or a DOM Element — so icon libraries that return nodes work directly with no extra conversion step.
Lucide (vanilla JS)
import { createElement, Zap } from "lucide";
// Pass an Element directly
notification.notify.success("Charged!", {
iconHtml: createElement(Zap),
});
// Or register once and use by name
notification.registerIcon("zap", createElement(Zap));
await notification.confirm.fire({ icon: "zap", title: "Quick action?" });Lucide React
import { renderToStaticMarkup } from "react-dom/server";
import { Zap } from "lucide-react";
notification.notify.success("Charged!", {
iconHtml: renderToStaticMarkup(<Zap size={20} />),
});Iconify
import { loadIcon, iconToSVG, iconToHTML } from "@iconify/utils";
const data = await loadIcon("mdi:flash");
notification.registerIcon("flash", iconToHTML(iconToSVG(data)));
await notification.confirm.fire({ icon: "flash", title: "Go!" });Iconify web component (DOM element)
const el = document.createElement("iconify-icon");
el.setAttribute("icon", "mdi:flash");
notification.notify.info("Tip", { iconHtml: el });Emoji or <img>
notification.notify.success("Deployed!", { iconHtml: "🚀" });
notification.notify.warning("Check this", {
iconHtml: '<img src="/logo.svg" width="20" alt="logo">',
});resolveIconHtml()
A utility that normalises any accepted icon value into a plain HTML string suitable for innerHTML. Useful when consuming the library alongside your own rendering logic.
import { resolveIconHtml } from "@subrotosaha/notification";
resolveIconHtml(null); // → ''
resolveIconHtml("…svg string…"); // → '…svg string…'
resolveIconHtml(domElement); // → element.outerHTMLTypeScript
Full type definitions are included. No @types package needed.
import notification, {
FireOptions,
FireResult,
NotifyOptions,
NotifyPosition,
NotifyVariant,
PromiseMessages,
ConfirmUpdateOptions,
DismissReason,
DismissReasonType,
MixinInstance,
QueueResult,
InputType,
IconType,
GlobalConfig,
registerIcon,
resolveIconHtml,
} from "@subrotosaha/notification";
// Typed confirm with preConfirm return value
const options: FireOptions<{ id: number; name: string }> = {
title: "Confirm action",
input: "text",
inputLabel: "Username",
confirmButtonText: "Submit",
showLoaderOnConfirm: true,
preConfirm: async (username) => {
const res = await fetch(`/api/users/${username}`);
if (!res.ok) throw new Error("User not found");
return res.json();
},
};
const result: FireResult<{ id: number; name: string }> =
await notification.confirm.fire(options);
if (result.isConfirmed) {
console.log(result.value?.name); // fully typed
}
// Typed toast
const notifyOptions: NotifyOptions = {
variant: "filled",
position: "bottom-right",
title: "Done",
duration: 3000,
progressBar: true,
action: { text: "Undo", onClick: () => undo() },
};
notification.notify.success("Saved!", notifyOptions);
// Typed promise toast
notification.notify.promise<{ count: number }>(
fetch("/api/save").then((r) => r.json()),
{
loading: "Saving…",
success: (data) => `Saved ${data.count} items`,
error: (err) => `Error: ${(err as Error).message}`,
} satisfies PromiseMessages<{ count: number }>
);
// Global config with type checking
notification.config({
toast: { position: "top-right", variant: "filled", duration: 5000 },
confirm: { confirmButtonText: "Yes", cancelButtonText: "No" },
} satisfies GlobalConfig);
// Dismiss reasons
if (result.dismiss === DismissReason.timer) {
console.log("Timed out");
}Changelog
Toast
notify.success(),notify.warning(),notify.error(),notify.info()notify.loading()— persistent spinner toast, manual dismiss requirednotify.dismiss(id)/notify.dismissAll()notify.promise(promise, messages, options?)— auto loading → success/error trackingNotifyOptions:title,variant,position,duration,pauseOnHover,progressBar,action,onClick,onClose,iconHtml,closable- Inline action button support (
action.text+action.onClick) - Variants:
default,filled,outlined - Positions:
top-right,top-left,top-center,bottom-right,bottom-left,bottom-center - Toasts stack and reposition automatically per position bucket
Confirm
confirm.fire(options)— full options APIconfirm.success/warning/error/info/question(options)— icon-preset shorthand methodsconfirm.mixin(defaults)— reusable pre-configured dialog factoryconfirm.queue(steps)— sequential multi-step dialogs with auto progress stepsconfirm.isVisible(),confirm.isLoading(),confirm.getPopup(),confirm.close()confirm.update(options)— live-update title, text, and button labels on an open dialog- 15 input types:
inputValidator,inputOptions,inputAttributes preConfirm+showLoaderOnConfirm— async confirm with loading statepreDeny— async deny hook, abort withreturn falsetimer+timerProgressBar— auto-close with draining progress bar- Multi-step dialogs:
progressSteps+currentProgressStep - Lifecycle hooks:
willOpen,didOpen,willClose,didClose DismissReasonconstant for type-safe dismiss comparisons- Variants:
default,filled,outlined
Icons
- 32 built-in SVG icons across 7 categories: status, UI controls, actions, navigation, feedback, identity, misc
registerIcon(name, svg: string | Element)— register custom icons by name; accepts a raw SVG string or any DOMElement(e.g. from Lucide, Iconify)resolveIconHtml(icon)— normalisesstring | Element | nullto an HTML string forinnerHTMLnotification.icons— liveRecord<string, string>registry of all built-ins + custom iconsiconHtmloption acceptsstring | Element | nullin bothNotifyOptionsandFireOptionsIconTypeincludes all built-in names with full autocomplete plus(string & {})for custom names
Build & TypeScript
- Full TypeScript source — zero
anytypes, strict mode - ESM-only output (
dist/) — no CommonJS bundle - Individual compiled files, no bundle merging:
confirm.js,toast.js,icons.js,types.js,index.js - Individual
.d.tsdeclarations per module - No
@typespackage required
