iOS-glass particle notifications — dots fly in from random positions, merge into a frosted-glass card, and explode back into dots on dismiss.
| Feature | Details |
|---|---|
| 🌊 Particle entry | 65 dots fly in from random positions with comet trails and glow halos |
| 💥 Particle exit | 52 dots explode outward on dismiss |
| 🧊 iOS glass card | backdrop-filter: blur(40px) saturate(220%) — real frosted glass |
| ⏸️ Hover pause | Timer freezes on card hover, dots pulse up |
| 🖼️ Developer images | Pass a URL, File object, or <img> element as the notification icon |
| 🎵 Web Audio sound | Zero-file sound via Web Audio API |
| 📦 Stack & queue | Up to 4 stacked, extras queue automatically |
| 👆 Swipe dismiss | Drag left/right to fling away |
| 🔘 Action buttons | Retry, Dismiss — any labels you want |
| 💍 Ring progress | Circular countdown ring around the icon as alternative to dots |
| 🎨 4 themes | ios-glass · dark · light · minimal |
| 🔷 TypeScript | Full .d.ts included |
| ⚡ Promise API | await notify.fire(...) — knows how it was dismissed |
| 0️⃣ Zero deps | Pure vanilla JS, no external libraries |
npm install dotnotifyOr via CDN (no build step):
<script src="https://cdn.jsdelivr.net/npm/dotnotify/dist/dotnotify.min.js"></script>import DotNotify from 'dotnotify';
const notify = DotNotify({ position: 'top-right' });
notify.fire({
type: 'success',
title: 'Logged in!',
message: 'Welcome back, Rahul.',
});That's it. Dots fly in, card appears, dots explode when it disappears.
const notify = DotNotify({
position: 'top-right', // 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'
theme: 'ios-glass', // 'ios-glass' | 'dark' | 'light' | 'minimal'
sound: false, // Web Audio ping on appear
maxStack: 4, // Max simultaneous notifications
duration: 4200, // Auto-dismiss after N milliseconds
zIndex: 99999, // CSS z-index
});const result = await notify.fire({
// ── Required ──────────────────────────────
type: 'error', // 'error' | 'success' | 'warning' | 'info'
title: 'Login Failed',
message: 'Invalid credentials. Please try again.',
// ── Icon / Image ──────────────────────────
image: 'https://example.com/avatar.png', // URL ← developer picture
// image: fileInput.files[0], // File ← from <input type="file">
// image: document.querySelector('#myImg'), // HTMLImageElement
// icon: '🔥', // Emoji (when no image)
// icon: '<svg>...</svg>', // SVG string
thumbnail: 'https://example.com/preview.jpg', // Small image beside message
// ── Override per-notification ─────────────
theme: 'ios-glass', // Override global theme
duration: 6000, // Override global duration (ms)
app: 'My App', // Override app label above title
sound: true, // Override global sound setting
progressStyle: 'dots', // 'dots' | 'ring'
// ── Action Buttons ────────────────────────
actions: [
{ label: 'Retry', primary: true, onClick: () => retryLogin() },
{ label: 'Dismiss', primary: false, onClick: () => {} },
],
});
// result = { dismissed: 'action' | 'timeout' | 'swipe-left' | 'swipe-right' | 'close', action?: 'Retry' | 'Dismiss' }
console.log(result.dismissed); // 'action'
console.log(result.action); // 'Retry'DotNotify lets developers put any image — user avatar, app logo, product thumbnail — into the notification icon area. Three ways to do it:
notify.fire({
type: 'info',
title: 'New message from Priya',
message: 'Hey, can we jump on a call?',
image: 'https://i.pravatar.cc/150?img=47', // any image URL
});<input type="file" id="avatar" accept="image/*">document.getElementById('avatar').addEventListener('change', (e) => {
const file = e.target.files[0];
notify.fire({
type: 'success',
title: 'Profile picture updated',
message: 'Your new avatar looks great!',
image: file, // ← File object directly, no FileReader needed
});
});document.addEventListener('drop', (e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
if (!file?.type.startsWith('image/')) return;
notify.fire({
type: 'info',
title: 'File received',
message: file.name,
image: file,
});
});const img = document.querySelector('#user-avatar');
notify.fire({
type: 'success',
title: 'Logged in',
message: 'Welcome back!',
image: img, // ← pass the element directly
});notify.fire({
type: 'info',
title: 'New photo shared',
message: 'Suresh shared a photo with you.',
image: 'https://example.com/suresh-avatar.jpg', // icon area
thumbnail: 'https://example.com/photo-preview.jpg', // beside message
});// Per-notification theme override:
notify.fire({ type: 'error', title: 'Oops', message: '...', theme: 'light' });| Theme | Look |
|---|---|
ios-glass |
Deep dark frosted glass — blur 40px, barely-visible tint |
dark |
Opaque near-black, strong shadow |
light |
White frosted glass, for light-background sites |
minimal |
Ultra-thin dark card, no sheen |
// Circular ring around the icon
notify.fire({
type: 'warning',
title: 'Session expiring',
message: 'You will be logged out in 60 seconds.',
progressStyle: 'ring', // ← ring traces around the icon
});
// Default dot progress bar (14 dots at bottom)
notify.fire({
type: 'info',
title: 'Syncing...',
message: 'Your data is being uploaded.',
progressStyle: 'dots', // default
});When a user hovers the card:
- The timer freezes (remembers exact progress, resumes on mouse-leave)
- All pending dots pulse up and glow in the accent color
- Done dots revive slightly
- Individual dot hover expands that dot to 12px with full glow bloom
Works on both desktop (click-drag) and mobile (touch). Drag left or right past 80px threshold — the card flings away and dots explode.
const result = await notify.fire({ ... });
if (result.dismissed === 'swipe-left') {
console.log('User swiped it away');
}import DotNotify from 'dotnotify';
import { useRef, useCallback } from 'react';
export function useNotify() {
const notifyRef = useRef(null);
function getNotify() {
if (!notifyRef.current) {
notifyRef.current = DotNotify({ position: 'top-right', theme: 'ios-glass' });
}
return notifyRef.current;
}
const fire = useCallback((opts) => getNotify().fire(opts), []);
return { fire };
}
// In any component:
function LoginForm() {
const { fire } = useNotify();
async function handleSubmit() {
try {
await loginAPI();
fire({ type: 'success', title: 'Welcome!', message: 'Logged in successfully.' });
} catch (err) {
const result = await fire({
type: 'error',
title: 'Login Failed',
message: err.message,
actions: [{ label: 'Retry', primary: true }],
});
if (result.action === 'Retry') handleSubmit();
}
}
return <button onClick={handleSubmit}>Login</button>;
}// plugins/notify.js
import DotNotify from 'dotnotify';
export const notify = DotNotify({ position: 'top-right', theme: 'ios-glass' });
// main.js
import { notify } from './plugins/notify';
app.config.globalProperties.$notify = notify;
// In component:
this.$notify.fire({ type: 'success', title: 'Done!', message: 'Saved.' });<script src="https://cdn.jsdelivr.net/npm/dotnotify/dist/dotnotify.min.js"></script>
<script>
const notify = DotNotify({ position: 'top-right' });
document.querySelector('#login-btn').addEventListener('click', async () => {
const result = await notify.fire({
type: 'error',
title: 'Login Failed',
message: 'Wrong password. Try again or reset.',
actions: [
{ label: 'Retry', primary: true },
{ label: 'Reset pass', onClick: () => location.href = '/reset' },
],
});
console.log(result); // { dismissed: 'action', action: 'Retry' }
});
</script>// Works only in browser — wrap in useEffect / onMounted
useEffect(() => {
const notify = DotNotify({ position: 'top-right' });
notify.fire({ type: 'info', title: 'Hello', message: 'Page loaded.' });
}, []);const notify = DotNotify({ ... });
// Fire a notification (returns Promise)
await notify.fire({ ... });
// Dismiss all visible notifications immediately
notify.dismissAll();
// Update global config after init
notify.configure({ theme: 'light', sound: true });DotNotify generates a subtle two-tone ping using the Web Audio API — no audio files, no network requests.
const notify = DotNotify({ sound: true }); // enable globally
notify.fire({ ..., sound: false }); // override per-notification
notify.fire({ ..., sound: true }); // override per-notificationSound frequencies by type:
error— 440Hz → 330Hz (descending, unsettling)success— 523Hz → 659Hz (ascending, happy)warning— 466Hz → 440Hz (slight drop, cautionary)info— 587Hz → 659Hz (gentle rise, neutral)
| Browser | Support |
|---|---|
| Chrome 76+ | ✅ Full |
| Safari 14+ | ✅ Full |
| Firefox 70+ | ✅ Full |
| Edge 79+ | ✅ Full |
| iOS Safari 14+ | ✅ Full (touch swipe works) |
| Android Chrome | ✅ Full |
backdrop-filterrequires Chrome 76+, Safari 9+, Firefox 70+. On older browsers the card still renders — just without the blur effect.
dotnotify/
├── dist/
│ ├── dotnotify.js ← UMD build (CommonJS + browser global)
│ ├── dotnotify.esm.js ← ES Module build (import/export)
│ ├── dotnotify.min.js ← Minified browser build (~9kb gzipped)
│ └── dotnotify.d.ts ← TypeScript definitions
├── src/
│ └── dotnotify.js ← Source
├── examples/
│ ├── vanilla.html ← Vanilla JS demo
│ └── react-example.jsx ← React demo
├── package.json
└── README.md
-
dotnotify/react— official React component wrapper -
dotnotify/vue— official Vue plugin - Custom dot count and dot size
- Notification center (history panel)
- CSS custom properties for full theme control
- RTL support
MIT © 2024 Your Name
- Fork the repo
npm install- Edit
src/dotnotify.js npm run build- Open
examples/vanilla.htmlto test - Submit a PR
