A playful close button with burst effects and floating bubbles for modals and popups
Click the X to see the close animation
Copy URL or Code
Paste to your AI coding assistant and customize:
Done. Your AI handles the rest.
Fully customizable. Features an expanding background, burst particles, floating bubbles, and swirling close animation. Perfect for modal dialogs, notifications, or playful UI elements.
/*
================================================================================
AI COMPONENT: PopClose
================================================================================
SETUP:
1. Install dependency: npm install @mojs/core
2. Create file: components/PopClose.tsx
3. Copy this entire code block into that file
4. Import: import PopClose from "@/components/PopClose"
================================================================================
QUICK CUSTOMIZATION (via props)
================================================================================
| Prop | Type | Default | Description |
|-----------------|---------------|-------------|----------------------------------|
| size | number | 320 | Component size in pixels |
| backgroundColor | string (hex) | "#28143F" | Container background color |
| accentColor | string (hex) | "#FC2D79" | Accent/burst color (pink) |
| autoPlay | boolean | true | Auto-start animation loop |
| loopDelay | number (ms) | 1200 | Delay between animation loops |
| className | string | "" | Additional CSS classes |
================================================================================
COMMON MODIFICATIONS
================================================================================
1. DEFAULT USAGE:
<PopClose />
2. CUSTOM COLORS:
<PopClose accentColor="#FF6B6B" backgroundColor="#1a1a2e" />
3. LARGER SIZE:
<PopClose size={400} />
4. FASTER LOOP:
<PopClose loopDelay={800} />
5. NO AUTO-PLAY (trigger manually):
<PopClose autoPlay={false} />
================================================================================
SOURCE CODE
================================================================================
*/
"use client";
import React, { useRef, useEffect, useCallback } from "react";
import mojs from "@mojs/core";
// =============================================================================
// POP CLOSE - Animated dismiss button with burst effects
// =============================================================================
// A playful close/dismiss button featuring:
// - Expanding colored background
// - X icon with burst particle effect
// - Floating bubbles animation
// - Swirling fade-out on close
//
// CUSTOMIZATION GUIDE FOR AI ASSISTANTS:
// - accentColor: Change the burst/background color (default: pink #FC2D79)
// - backgroundColor: Change the container background (default: dark purple #28143F)
// - size: Resize the component (default: 320px)
// - loopDelay: Time between animation loops in ms (default: 1200)
// - autoPlay: Start animation automatically (default: true)
// =============================================================================
export interface PopCloseProps {
/** Component size in pixels */
size?: number;
/** Container background color */
backgroundColor?: string;
/** Accent color for burst and background expansion */
accentColor?: string;
/** Additional CSS classes */
className?: string;
/** Auto-start the animation loop */
autoPlay?: boolean;
/** Delay between animation loops in ms */
loopDelay?: number;
}
export default function PopClose({
size = 320,
backgroundColor = "#28143F",
accentColor = "#FC2D79",
className = "",
autoPlay = true,
loopDelay = 1200,
}: PopCloseProps) {
const containerRef = useRef<HTMLDivElement>(null);
const animationsRef = useRef<{
openTimeline: mojs.Timeline | null;
closeTimeline: mojs.Timeline | null;
openBackground: mojs.Shape | null;
circle: mojs.Shape | null;
}>({
openTimeline: null,
closeTimeline: null,
openBackground: null,
circle: null,
});
const isInitializedRef = useRef(false);
const initAnimation = useCallback(() => {
if (!containerRef.current || isInitializedRef.current) return;
isInitializedRef.current = true;
const container = containerRef.current;
// Animation timing constant - controls overall speed
const ANIM_DURATION = 380;
// Clear any existing children
container.innerHTML = "";
let loopTimer: ReturnType<typeof setTimeout> | null = null;
// === OPENING ANIMATION ===
// Expanding circular background that fills the container
const openBackground = new mojs.Shape({
parent: container,
fill: accentColor,
scale: { 0: 4.2 },
isForce3d: true,
isTimelineLess: true,
radius: Math.min(size, size) * 0.28,
easing: "cubic.out",
backwardEasing: "expo.in",
duration: 2 * ANIM_DURATION,
left: "50%",
top: "50%",
onStart(isForward: boolean) {
if (!isForward) {
clearTimeout(loopTimer!);
loopTimer = setTimeout(() => {
animationsRef.current.openTimeline?.replay();
}, loopDelay);
}
},
});
// Shared visual options for stroke-based elements
const STROKE_OPTS = {
fill: "none",
stroke: "white",
isTimelineLess: true,
};
// Circle outline that appears with the X button
const circle = new mojs.Shape({
...STROKE_OPTS,
parent: container,
left: "72%",
top: "28%",
radius: { 0: 14 },
easing: "cubic.out",
strokeWidth: { 9: 0 },
duration: 1.5 * ANIM_DURATION,
className: "pop-dismiss-trigger",
});
// X icon (cross rotated 45 degrees)
const xIcon = new mojs.Shape({
...STROKE_OPTS,
parent: circle.el,
shape: "cross",
radius: { 0: 7 },
angle: 45,
duration: ANIM_DURATION,
delay: 0.35 * ANIM_DURATION,
});
// Burst effect - lines radiating outward
const burst = new mojs.Burst({
parent: circle.el,
radius: { 0: 28 },
children: {
...STROKE_OPTS,
shape: "line",
scaleY: 1,
},
});
// Bubble options - small circles that pop up around the X
const BUBBLE_OPTS = {
...STROKE_OPTS,
parent: circle.el,
strokeWidth: { 4: 0 },
};
const bubbleTimeline = new mojs.Timeline({ delay: 0.65 * ANIM_DURATION });
// Three decorative bubbles at different positions
const bubble1 = new mojs.Shape({
...BUBBLE_OPTS,
radius: { 0: 9 },
left: "5%",
top: "-12%",
});
const bubble2 = new mojs.Shape({
...BUBBLE_OPTS,
radius: { 0: 5 },
delay: 0.35 * ANIM_DURATION,
left: "68%",
top: "5%",
});
const bubble3 = new mojs.Shape({
...BUBBLE_OPTS,
radius: { 0: 3 },
delay: 0.2 * ANIM_DURATION,
left: "48%",
top: "95%",
});
bubbleTimeline.add(bubble1, bubble2, bubble3);
// Main open timeline - orchestrates the opening sequence
const openTimeline = new mojs.Timeline({ speed: 1.2 });
const closeButtonTimeline = new mojs.Timeline({
delay: ANIM_DURATION / 2,
});
closeButtonTimeline.add(xIcon, circle, burst, bubbleTimeline);
openTimeline.add(openBackground, closeButtonTimeline);
// === CLOSING ANIMATION ===
// Circle that appears during close
const closeCircle = new mojs.Shape({
...STROKE_OPTS,
parent: container,
left: "72%",
top: "28%",
radius: { 0: 14 },
easing: "cubic.out",
strokeWidth: { 4: 0 },
duration: 1.5 * ANIM_DURATION,
className: "pop-dismiss-trigger",
isShowEnd: false,
});
// Swirling particles that float away on close
const SWIRL_OPTS = {
parent: closeCircle.el,
y: { 0: -90 },
fill: "white",
pathScale: "rand(0.3, 0.7)",
radius: "rand(10, 14)",
scale: { 1: 0 },
delay: "rand(0, 80)",
isForce3d: true,
duration: 1.5 * ANIM_DURATION,
swirlSize: "rand(8, 14)",
swirlFrequency: "rand(2, 5)",
};
const fadeTimeline = new mojs.Timeline({ delay: 0.12 * ANIM_DURATION });
// Create 4 swirling particles for close effect
for (let i = 0; i < 4; i++) {
fadeTimeline.add(
new mojs.ShapeSwirl({
...SWIRL_OPTS,
direction: i % 2 === 0 ? 1 : -1,
})
);
}
// X that shrinks away on close
const closeX = new mojs.Shape({
...STROKE_OPTS,
parent: closeCircle.el,
shape: "cross",
radius: { 7: 0 },
angle: 45,
duration: ANIM_DURATION,
delay: 0.35 * ANIM_DURATION,
isShowStart: true,
});
const closeTimeline = new mojs.Timeline();
closeTimeline.add(closeX, closeCircle, fadeTimeline);
// Store references for external control
animationsRef.current = {
openTimeline,
closeTimeline,
openBackground,
circle,
};
// Start animation if autoPlay is enabled
if (autoPlay) {
openTimeline.replay();
}
// Click handler - triggers close animation
const handleClick = () => {
circle._hide();
openTimeline.stop();
closeTimeline.replay();
openBackground.replayBackward();
};
circle.el.addEventListener("click", handleClick);
return () => {
circle.el.removeEventListener("click", handleClick);
if (loopTimer) clearTimeout(loopTimer);
};
}, [size, accentColor, autoPlay, loopDelay]);
useEffect(() => {
const cleanup = initAnimation();
return () => {
if (cleanup) cleanup();
isInitializedRef.current = false;
};
}, [initAnimation]);
return (
<div
ref={containerRef}
className={`relative ${className}`}
style={{
width: size,
height: size,
backgroundColor,
overflow: "hidden",
}}
/>
);
}
export { PopClose };import PopClose from "@/components/PopClose";
// Basic usage - loops automatically, click X to see close animation
export default function BasicExample() {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-900">
<PopClose />
</div>
);
}
// Custom colors - coral accent on dark blue background
export function CustomColors() {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-900">
<PopClose
accentColor="#FF6B6B"
backgroundColor="#1a1a2e"
/>
</div>
);
}
// Larger size with faster loop
export function LargerFaster() {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-900">
<PopClose size={400} loopDelay={800} />
</div>
);
}
// Use in a modal - disable autoPlay, trigger on demand
export function ModalCloseButton() {
return (
<div className="relative bg-white rounded-2xl p-8">
<div className="absolute top-4 right-4">
<PopClose
size={60}
autoPlay={false}
backgroundColor="transparent"
accentColor="#ef4444"
/>
</div>
<h2>Modal Content</h2>
</div>
);
}