Edge-attached notifications with 3D flip animations — 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 Flip (smooth 3D rotation) and Bouncy Flip (elastic spring) animations. Attaches to top or bottom edges with responsive mobile support.
/*
================================================================================
AI COMPONENT: FlipBannerNotification
================================================================================
SETUP:
1. Create file: components/FlipBannerNotification.tsx
2. Copy this entire code block into that file
3. Import: import FlipBannerNotification from "@/components/FlipBannerNotification"
4. No external dependencies required - pure React + CSS
================================================================================
QUICK CUSTOMIZATION (via props)
================================================================================
| Prop | Type | Default |
|------------|-------------------------------------------|-----------|
| message | string (supports HTML) | required |
| effect | "flip" | "bouncyflip" | "flip" |
| position | "top" | "bottom" | "top" |
| 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 |
|-------------|----------------------------------------------|
| flip | Smooth 3D perspective rotation |
| bouncyflip | Elastic spring-like flip with overshoot |
================================================================================
SOURCE CODE
================================================================================
*/
"use client";
import React, { useState, useEffect, useCallback, useRef } from "react";
export type FlipEffect = "flip" | "bouncyflip";
export type FlipPosition = "top" | "bottom";
export type NotificationType = "notice" | "warning" | "error" | "success";
export interface FlipBannerNotificationProps {
message: string;
effect?: FlipEffect;
position?: FlipPosition;
type?: NotificationType;
ttl?: number;
onClose?: () => void;
isVisible: boolean;
onDismiss: () => void;
}
const typeColors: Record<NotificationType, { bg: string; accent: string }> = {
notice: { bg: "rgba(42, 45, 50, 0.95)", accent: "#64B4FF" },
warning: { bg: "rgba(255, 200, 50, 0.95)", accent: "#FF9800" },
error: { bg: "rgba(220, 60, 60, 0.95)", accent: "#FF5252" },
success: { bg: "rgba(50, 180, 80, 0.95)", accent: "#4CAF50" },
};
export default function FlipBannerNotification({
message,
effect = "flip",
position = "top",
type = "notice",
ttl = 6000,
onClose,
isVisible,
onDismiss,
}: FlipBannerNotificationProps) {
const [show, setShow] = useState(false);
const [hide, setHide] = useState(false);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const colors = typeColors[type];
const isTop = position === "top";
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?.(); }, 400);
}, [onDismiss, onClose]);
if (!isVisible && !show) return null;
return (
<>
<style>{`
.flip-banner {
position: fixed;
${isTop ? "top: 0;" : "bottom: 0;"}
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: ${isTop ? "0 0 5px 5px" : "5px 5px 0 0"};
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
border-${isTop ? "bottom" : "top"}: 3px solid ${colors.accent};
opacity: 0;
transform-origin: ${isTop ? "50% 0%" : "50% 100%"};
}
.flip-banner p { margin: 0; line-height: 1.3; }
.flip-banner a { color: inherit; opacity: 0.85; font-weight: 600; text-decoration: underline; }
.flip-close {
width: 20px; height: 20px;
position: absolute; right: 8px; top: 8px;
cursor: pointer; opacity: 0.6;
}
.flip-close:hover { opacity: 1; }
.flip-close::before, .flip-close::after {
content: ''; position: absolute;
width: 2px; height: 12px; top: 50%; left: 50%;
background: ${type === "warning" ? "#333" : "#fff"};
}
.flip-close::before { transform: translate(-50%,-50%) rotate(45deg); }
.flip-close::after { transform: translate(-50%,-50%) rotate(-45deg); }
.ns-effect-flip {
transform: perspective(1000px) rotateX(${isTop ? "-90deg" : "90deg"});
transition: transform 0.4s cubic-bezier(0.4,0,0.2,1), opacity 0.3s;
}
.ns-effect-flip.ns-show { transform: perspective(1000px) rotateX(0); opacity: 1; }
.ns-effect-flip.ns-hide { transform: perspective(1000px) rotateX(${isTop ? "-90deg" : "90deg"}); opacity: 0; }
.ns-effect-bouncyflip {
transform: perspective(1000px) rotateX(${isTop ? "-90deg" : "90deg"});
animation-duration: 0.6s;
animation-fill-mode: forwards;
}
.ns-effect-bouncyflip.ns-show { animation-name: bouncyFlipIn; }
.ns-effect-bouncyflip.ns-hide { animation-name: bouncyFlipOut; }
@keyframes bouncyFlipIn {
0% { transform: perspective(1000px) rotateX(${isTop ? "-90deg" : "90deg"}); opacity: 0; }
40% { transform: perspective(1000px) rotateX(${isTop ? "20deg" : "-20deg"}); opacity: 1; }
60% { transform: perspective(1000px) rotateX(${isTop ? "-10deg" : "10deg"}); }
80% { transform: perspective(1000px) rotateX(${isTop ? "5deg" : "-5deg"}); }
100% { transform: perspective(1000px) rotateX(0); opacity: 1; }
}
@keyframes bouncyFlipOut {
0% { transform: perspective(1000px) rotateX(0); opacity: 1; }
100% { transform: perspective(1000px) rotateX(${isTop ? "-90deg" : "90deg"}); opacity: 0; }
}
@media (max-width: 400px) {
.flip-banner { left: 15px; right: 15px; max-width: none; }
}
`}</style>
<div className={`flip-banner ns-effect-${effect} ${show ? "ns-show" : ""} ${hide ? "ns-hide" : ""}`}>
<div className="flip-close" onClick={handleDismiss} />
<p dangerouslySetInnerHTML={{ __html: message }} />
</div>
</>
);
}import { useState, useCallback, useRef } from "react";
import FlipBannerNotification from "@/components/FlipBannerNotification";
export default function NotificationDemo() {
const [notifications, setNotifications] = useState([]);
const idRef = useRef(0);
const showNotification = useCallback((message, type = "notice", effect = "flip", position = "top") => {
const newId = ++idRef.current;
setNotifications(prev => [...prev, { id: newId, message, type, effect, position }]);
}, []);
const dismissNotification = useCallback((id) => {
setNotifications(prev => prev.filter(n => n.id !== id));
}, []);
return (
<div>
{/* Trigger buttons */}
<button onClick={() => showNotification("Event added!", "success", "flip", "top")}>
Top Flip
</button>
<button onClick={() => showNotification("Warning message", "warning", "bouncyflip", "bottom")}>
Bottom Bouncy
</button>
{/* Render notifications */}
{notifications.map(n => (
<FlipBannerNotification
key={n.id}
message={n.message}
effect={n.effect}
position={n.position}
type={n.type}
ttl={6000}
isVisible={true}
onDismiss={() => dismissNotification(n.id)}
/>
))}
</div>
);
}