Unique animated notifications with loaders and transitions — 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. Features Corner Expand (scale from corner), Loading Circle (SVG progress), Box Spinner (3D rotation), and Thumb Slider (image card slide-in) effects.
/*
================================================================================
AI COMPONENT: CreativeNotification
================================================================================
SETUP:
1. Create file: components/CreativeNotification.tsx
2. Copy this entire code block into that file
3. Import: import CreativeNotification from "@/components/CreativeNotification"
4. No external dependencies required - pure React + CSS
================================================================================
QUICK CUSTOMIZATION (via props)
================================================================================
| Prop | Type | Default |
|------------|---------------------------------------------------------|----------------|
| message | string (supports HTML) | required |
| effect | "cornerexpand" | "loadingcircle" | "boxspinner" | "thumbslider" | "cornerexpand" |
| type | "notice" | "warning" | "error" | "success" | "notice" |
| ttl | number (milliseconds, 0 = no auto-close) | 9000 |
| thumbnail | string (URL for thumbslider effect) | undefined |
| isVisible | boolean | required |
| onDismiss | () => void | required |
================================================================================
ANIMATION EFFECTS
================================================================================
| Effect | Description |
|---------------|---------------------------------------------------|
| cornerexpand | Scales up from bottom-right corner |
| loadingcircle | SVG circle progress then notification appears |
| boxspinner | 3D rotating box then notification flips in |
| thumbslider | Slides in from right with thumbnail image |
================================================================================
SOURCE CODE
================================================================================
*/
"use client";
import React, { useState, useEffect, useCallback, useRef } from "react";
export type CreativeEffect = "cornerexpand" | "loadingcircle" | "boxspinner" | "thumbslider";
export type NotificationType = "notice" | "warning" | "error" | "success";
export interface CreativeNotificationProps {
message: string;
effect?: CreativeEffect;
type?: NotificationType;
ttl?: number;
onClose?: () => void;
isVisible: boolean;
onDismiss: () => void;
thumbnail?: string;
}
const typeColors = {
notice: { bg: "#2a2d32", accent: "#667eea", text: "#fff" },
warning: { bg: "#fff3cd", accent: "#ffc107", text: "#856404" },
error: { bg: "#f8d7da", accent: "#dc3545", text: "#721c24" },
success: { bg: "#d4edda", accent: "#28a745", text: "#155724" },
};
export default function CreativeNotification({
message, effect = "cornerexpand", type = "notice", ttl = 9000, onClose, isVisible, onDismiss, thumbnail,
}: CreativeNotificationProps) {
const [show, setShow] = useState(false);
const [hide, setHide] = useState(false);
const [circleProgress, setCircleProgress] = useState(0);
const [spinnerRotation, setSpinnerRotation] = useState(0);
const timerRef = useRef(null);
const animRef = useRef(null);
const colors = typeColors[type];
const circumference = 2 * Math.PI * 30;
const strokeDashoffset = circumference - (circleProgress / 100) * circumference;
useEffect(() => {
if (isVisible) {
setHide(false); setCircleProgress(0); setSpinnerRotation(0);
if (effect === "loadingcircle") {
const startTime = Date.now(); const duration = 1500;
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
setCircleProgress(eased * 100);
if (progress < 1) animRef.current = setTimeout(animate, 16);
else setShow(true);
};
animRef.current = setTimeout(animate, 16);
} else if (effect === "boxspinner") {
const startTime = Date.now(); const duration = 800;
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
setSpinnerRotation(progress * 360);
if (progress < 1) animRef.current = setTimeout(animate, 16);
else setShow(true);
};
animRef.current = setTimeout(animate, 16);
} else {
requestAnimationFrame(() => setShow(true));
}
if (ttl > 0) {
const delay = effect === "loadingcircle" ? 1500 : effect === "boxspinner" ? 800 : 0;
timerRef.current = setTimeout(() => handleDismiss(), ttl + delay);
}
}
return () => { if (timerRef.current) clearTimeout(timerRef.current); if (animRef.current) clearTimeout(animRef.current); };
}, [isVisible, ttl, effect]);
const handleDismiss = useCallback(() => {
if (timerRef.current) clearTimeout(timerRef.current);
if (animRef.current) clearTimeout(animRef.current);
setHide(true);
setTimeout(() => { setShow(false); setCircleProgress(0); setSpinnerRotation(0); onDismiss(); onClose?.(); }, 500);
}, [onDismiss, onClose]);
if (!isVisible && !show && circleProgress === 0 && spinnerRotation === 0) return null;
return (
<>
<style>{`
.creative-notification {
position: fixed; max-width: 320px; background: ${colors.bg}; padding: 20px 36px 20px 20px;
border-radius: 8px; box-shadow: 0 8px 32px rgba(0,0,0,0.25); color: ${colors.text};
font-size: 14px; font-family: 'Helvetica Neue', sans-serif; z-index: 1000; opacity: 0;
}
.creative-notification.corner-expand {
bottom: 20px; right: 20px; transform: scale(0) translate(50%,50%); transform-origin: bottom right;
transition: transform 0.5s cubic-bezier(0.34,1.56,0.64,1), opacity 0.3s;
}
.creative-notification.corner-expand.ns-show { transform: scale(1) translate(0,0); opacity: 1; }
.creative-notification.corner-expand.ns-hide { transform: scale(0) translate(50%,50%); opacity: 0; }
.loading-circle-wrapper { position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); z-index: 1000; }
.loading-circle-svg { width: 70px; height: 70px; transform: rotate(-90deg); }
.loading-circle-bg { fill: none; stroke: rgba(100,126,234,0.2); stroke-width: 4; }
.loading-circle-progress { fill: none; stroke: ${colors.accent}; stroke-width: 4; stroke-linecap: round; }
.creative-notification.loading-circle {
top: 50%; left: 50%; transform: translate(-50%,-50%) scale(0.8);
transition: transform 0.4s cubic-bezier(0.34,1.56,0.64,1), opacity 0.3s;
}
.creative-notification.loading-circle.ns-show { transform: translate(-50%,-50%) scale(1); opacity: 1; }
.creative-notification.loading-circle.ns-hide { transform: translate(-50%,-50%) scale(0.8); opacity: 0; }
.box-spinner-wrapper { position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); z-index: 1000; perspective: 1000px; }
.box-spinner { width: 50px; height: 50px; background: ${colors.accent}; border-radius: 4px; }
.creative-notification.box-spinner-notif {
top: 50%; left: 50%; transform: translate(-50%,-50%) rotateY(90deg);
transition: transform 0.5s cubic-bezier(0.34,1.56,0.64,1), opacity 0.3s;
}
.creative-notification.box-spinner-notif.ns-show { transform: translate(-50%,-50%) rotateY(0); opacity: 1; }
.creative-notification.box-spinner-notif.ns-hide { transform: translate(-50%,-50%) rotateY(90deg); opacity: 0; }
.creative-notification.thumb-slider {
bottom: 20px; right: 20px; max-width: 360px; padding: 0;
transform: translateX(120%); display: flex; overflow: hidden;
transition: transform 0.5s cubic-bezier(0.34,1.56,0.64,1), opacity 0.3s;
}
.creative-notification.thumb-slider.ns-show { transform: translateX(0); opacity: 1; }
.creative-notification.thumb-slider.ns-hide { transform: translateX(120%); opacity: 0; }
.thumb-slider-image { width: 100px; min-height: 80px; object-fit: cover; background: ${colors.accent}; }
.thumb-slider-content { padding: 16px 32px 16px 16px; flex: 1; }
.creative-close { width: 20px; height: 20px; position: absolute; right: 10px; top: 10px; cursor: pointer; opacity: 0.6; }
.creative-close:hover { opacity: 1; }
.creative-close::before, .creative-close::after { content: ''; position: absolute; width: 2px; height: 12px; top: 50%; left: 50%; background: ${colors.text}; }
.creative-close::before { transform: translate(-50%,-50%) rotate(45deg); }
.creative-close::after { transform: translate(-50%,-50%) rotate(-45deg); }
.notif-content p { margin: 0; line-height: 1.4; }
.notif-content a { color: ${colors.accent}; font-weight: 600; text-decoration: none; }
`}</style>
{effect === "loadingcircle" && circleProgress > 0 && circleProgress < 100 && (
<div className="loading-circle-wrapper">
<svg className="loading-circle-svg" viewBox="0 0 70 70">
<circle className="loading-circle-bg" cx="35" cy="35" r="30" />
<circle className="loading-circle-progress" cx="35" cy="35" r="30" strokeDasharray={circumference} strokeDashoffset={strokeDashoffset} />
</svg>
</div>
)}
{effect === "boxspinner" && spinnerRotation > 0 && spinnerRotation < 360 && (
<div className="box-spinner-wrapper"><div className="box-spinner" style={{ transform: `rotateY(${spinnerRotation}deg)` }} /></div>
)}
{effect === "cornerexpand" && (
<div className={`creative-notification corner-expand ${show ? "ns-show" : ""} ${hide ? "ns-hide" : ""}`}>
<div className="creative-close" onClick={handleDismiss} />
<div className="notif-content"><p dangerouslySetInnerHTML={{ __html: message }} /></div>
</div>
)}
{effect === "loadingcircle" && circleProgress >= 100 && (
<div className={`creative-notification loading-circle ${show ? "ns-show" : ""} ${hide ? "ns-hide" : ""}`}>
<div className="creative-close" onClick={handleDismiss} />
<div className="notif-content"><p dangerouslySetInnerHTML={{ __html: message }} /></div>
</div>
)}
{effect === "boxspinner" && spinnerRotation >= 360 && (
<div className={`creative-notification box-spinner-notif ${show ? "ns-show" : ""} ${hide ? "ns-hide" : ""}`}>
<div className="creative-close" onClick={handleDismiss} />
<div className="notif-content"><p dangerouslySetInnerHTML={{ __html: message }} /></div>
</div>
)}
{effect === "thumbslider" && (
<div className={`creative-notification thumb-slider ${show ? "ns-show" : ""} ${hide ? "ns-hide" : ""}`}>
{thumbnail ? <img src={thumbnail} alt="" className="thumb-slider-image" /> : <div className="thumb-slider-image" />}
<div className="thumb-slider-content">
<div className="creative-close" onClick={handleDismiss} />
<div className="notif-content"><p dangerouslySetInnerHTML={{ __html: message }} /></div>
</div>
</div>
)}
</>
);
}import { useState, useCallback, useRef } from "react";
import CreativeNotification from "@/components/CreativeNotification";
export default function NotificationDemo() {
const [notification, setNotification] = useState(null);
const idRef = useRef(0);
const showNotification = useCallback((message, effect = "cornerexpand", type = "notice", thumbnail) => {
const newId = ++idRef.current;
setNotification({ id: newId, message, effect, type, thumbnail });
}, []);
const dismissNotification = useCallback(() => {
setNotification(null);
}, []);
return (
<div>
{/* Trigger buttons */}
<button onClick={() => showNotification("Expanded from corner!", "cornerexpand", "notice")}>
Corner Expand
</button>
<button onClick={() => showNotification("Loading complete!", "loadingcircle", "success")}>
Loading Circle
</button>
<button onClick={() => showNotification("Spinning in!", "boxspinner", "notice")}>
Box Spinner
</button>
<button onClick={() => showNotification("New photo!", "thumbslider", "success", "/image.jpg")}>
Thumb Slider
</button>
{/* Render notification */}
{notification && (
<CreativeNotification
message={notification.message}
effect={notification.effect}
type={notification.type}
thumbnail={notification.thumbnail}
ttl={9000}
isVisible={true}
onDismiss={dismissNotification}
/>
)}
</div>
);
}