Corner-positioned notifications with 4 animation styles — add to your site in seconds
Copy URL or Code
Paste to your AI coding assistant and customize:
Done. Your AI handles the rest.
Fully customizable. Includes Scale (pop), Jelly (bounce), Slide (smooth), and Genie (3D flip) animations. Supports notice, warning, error, and success notification types.
/*
================================================================================
AI COMPONENT: GrowlToastNotification
================================================================================
SETUP:
1. Create file: components/GrowlToastNotification.tsx
2. Copy this entire code block into that file
3. Import: import GrowlToastNotification from "@/components/GrowlToastNotification"
4. No external dependencies required - pure React + CSS
================================================================================
QUICK CUSTOMIZATION (via props)
================================================================================
| Prop | Type | Default |
|------------|-------------------------------------------|-----------|
| message | string (supports HTML) | required |
| effect | "scale" | "jelly" | "slide" | "genie" | "scale" |
| type | "notice" | "warning" | "error" | "success"| "notice" |
| ttl | number (milliseconds, 0 = no auto-close) | 6000 |
| isVisible | boolean | required |
| onDismiss | () => void | required |
| onClose | () => void | undefined |
================================================================================
ANIMATION EFFECTS
================================================================================
| Effect | Description |
|---------|------------------------------------------------|
| scale | Smooth scale-up/down animation |
| jelly | Bouncy elastic animation |
| slide | Slides in from the left |
| genie | 3D perspective flip animation |
================================================================================
NOTIFICATION TYPES (colors)
================================================================================
| Type | Background Color | Use Case |
|---------|----------------------------|---------------------------|
| notice | Dark gray (rgba 42,45,50) | General information |
| warning | Yellow (rgba 255,200,50) | Warnings/cautions |
| error | Red (rgba 220,60,60) | Errors/failures |
| success | Green (rgba 50,180,80) | Success/confirmations |
================================================================================
SOURCE CODE
================================================================================
*/
"use client";
import React, { useState, useEffect, useCallback, useRef } from "react";
export type GrowlEffect = "scale" | "jelly" | "slide" | "genie";
export type NotificationType = "notice" | "warning" | "error" | "success";
export interface GrowlToastNotificationProps {
message: string;
effect?: GrowlEffect;
type?: NotificationType;
ttl?: number;
onClose?: () => void;
isVisible: boolean;
onDismiss: () => void;
}
const typeColors: Record<NotificationType, { bg: string; border: string }> = {
notice: { bg: "rgba(42, 45, 50, 0.95)", border: "rgba(100, 180, 255, 0.5)" },
warning: { bg: "rgba(255, 200, 50, 0.95)", border: "rgba(255, 180, 0, 0.8)" },
error: { bg: "rgba(220, 60, 60, 0.95)", border: "rgba(180, 40, 40, 0.8)" },
success: { bg: "rgba(50, 180, 80, 0.95)", border: "rgba(40, 150, 60, 0.8)" },
};
export default function GrowlToastNotification({
message,
effect = "scale",
type = "notice",
ttl = 6000,
onClose,
isVisible,
onDismiss,
}: GrowlToastNotificationProps) {
const [show, setShow] = useState(false);
const [hide, setHide] = useState(false);
const timerRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (isVisible) {
setHide(false);
requestAnimationFrame(() => setShow(true));
if (ttl > 0) {
timerRef.current = setTimeout(() => handleDismiss(), ttl);
}
}
return () => { if (timerRef.current) clearTimeout(timerRef.current); };
}, [isVisible, ttl]);
const handleDismiss = useCallback(() => {
if (timerRef.current) clearTimeout(timerRef.current);
setHide(true);
setTimeout(() => {
setShow(false);
onDismiss();
onClose?.();
}, 300);
}, [onDismiss, onClose]);
if (!isVisible && !show) return null;
const colors = typeColors[type];
const effectClass = `ns-effect-${effect}`;
return (
<>
<style>{`
.toast-notification {
position: fixed;
top: 30px;
left: 30px;
max-width: 300px;
background: ${colors.bg};
padding: 22px 36px 22px 22px;
line-height: 1.4;
z-index: 1000;
color: ${type === "warning" ? "rgba(40,40,40,0.95)" : "rgba(250,251,255,0.95)"};
font-size: 14px;
font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif;
border-radius: 5px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
border-left: 3px solid ${colors.border};
opacity: 0;
}
.toast-notification p { margin: 0; line-height: 1.3; }
.toast-notification a { color: inherit; opacity: 0.85; font-weight: 600; text-decoration: underline; }
.toast-notification a:hover { opacity: 1; }
.toast-close {
width: 20px; height: 20px;
position: absolute; right: 8px; top: 8px;
cursor: pointer; opacity: 0.6;
}
.toast-close:hover { opacity: 1; }
.toast-close::before, .toast-close::after {
content: ''; position: absolute;
width: 2px; height: 12px; top: 50%; left: 50%;
background: ${type === "warning" ? "#333" : "#fff"};
}
.toast-close::before { transform: translate(-50%,-50%) rotate(45deg); }
.toast-close::after { transform: translate(-50%,-50%) rotate(-45deg); }
.ns-effect-scale { transform: scale(0.4); transition: transform 0.3s cubic-bezier(0.6,0.2,0.1,1), opacity 0.3s; }
.ns-effect-scale.ns-show { transform: scale(1); opacity: 1; }
.ns-effect-scale.ns-hide { transform: scale(0.4); opacity: 0; }
.ns-effect-jelly { transform: scale(0); animation-duration: 0.5s; animation-fill-mode: forwards; }
.ns-effect-jelly.ns-show { animation-name: jellyIn; }
.ns-effect-jelly.ns-hide { animation-name: jellyOut; }
@keyframes jellyIn { 0%{transform:scale(0);opacity:0} 30%{transform:scale(1.1);opacity:1} 50%{transform:scale(0.95)} 70%{transform:scale(1.02)} 100%{transform:scale(1);opacity:1} }
@keyframes jellyOut { 0%{transform:scale(1);opacity:1} 30%{transform:scale(1.05)} 100%{transform:scale(0);opacity:0} }
.ns-effect-slide { transform: translateX(-100%); transition: transform 0.4s cubic-bezier(0.4,0,0.2,1), opacity 0.3s; }
.ns-effect-slide.ns-show { transform: translateX(0); opacity: 1; }
.ns-effect-slide.ns-hide { transform: translateX(-100%); opacity: 0; }
.ns-effect-genie { transform-origin: 0% 0%; transform: rotateY(-70deg) scale(0.6); transition: transform 0.4s cubic-bezier(0.6,0.2,0.1,1), opacity 0.3s; }
.ns-effect-genie.ns-show { transform: rotateY(0) scale(1); opacity: 1; }
.ns-effect-genie.ns-hide { transform: rotateY(-70deg) scale(0.6); opacity: 0; }
`}</style>
<div className={`toast-notification ${effectClass} ${show ? "ns-show" : ""} ${hide ? "ns-hide" : ""}`}>
<div className="toast-close" onClick={handleDismiss} />
<p dangerouslySetInnerHTML={{ __html: message }} />
</div>
</>
);
}import { useState, useCallback, useRef } from "react";
import GrowlToastNotification from "@/components/GrowlToastNotification";
// Basic usage with state management
export default function NotificationDemo() {
const [notifications, setNotifications] = useState([]);
const idRef = useRef(0);
const showNotification = useCallback((message, type = "notice", effect = "scale") => {
const newId = ++idRef.current;
setNotifications(prev => [...prev, { id: newId, message, type, effect }]);
}, []);
const dismissNotification = useCallback((id) => {
setNotifications(prev => prev.filter(n => n.id !== id));
}, []);
return (
<div>
{/* Trigger buttons */}
<button onClick={() => showNotification("Settings saved!", "success", "scale")}>
Success (Scale)
</button>
<button onClick={() => showNotification("Please check your input", "warning", "jelly")}>
Warning (Jelly)
</button>
<button onClick={() => showNotification("Something went wrong", "error", "slide")}>
Error (Slide)
</button>
<button onClick={() => showNotification("New message received", "notice", "genie")}>
Notice (Genie)
</button>
{/* Render notifications */}
{notifications.map(n => (
<GrowlToastNotification
key={n.id}
message={n.message}
effect={n.effect}
type={n.type}
ttl={6000}
isVisible={true}
onDismiss={() => dismissNotification(n.id)}
/>
))}
</div>
);
}