--- title: 3d Image Pageflip description: An interactive 3D book and magazine page flip component featuring dynamic spine shifting, realistic lighting crease shadows, hover peeking, and leaf stacking. --- ## Installation CLI Manual ```bash npx lightswind@latest add 3d-image-pageflip ``` Copy and paste the following code into your project. ```tsx "use client"; import React, { useState, useEffect, useCallback, CSSProperties, forwardRef, useImperativeHandle } from "react"; import { cn } from "@/components/lib/utils"; import { ChevronLeft, ChevronRight, RotateCcw } from "lucide-react"; export interface PageFlipLeaf { id?: string | number; frontImage: string; backImage: string; frontTitle?: string; backTitle?: string; frontSubtitle?: string; backSubtitle?: string; frontBadge?: string; backBadge?: string; } export interface ThreeDImagePageflipProps { /** Array of page leaves, each containing front and back images & metadata */ pages?: PageFlipLeaf[]; /** Default turned page count (0 = closed book on cover) */ defaultTurnedIndex?: number; /** Controlled turned page count */ turnedIndex?: number; /** Callback fired when page flip changes */ onPageChange?: (turnedCount: number, totalLeaves: number) => void; /** Width of a single page in pixels (default: 230) */ pageWidth?: number; /** Height of a single page in pixels (default: 330) */ pageHeight?: number; /** 3D perspective depth in pixels (default: 1300) */ perspective?: number; /** Maximum hover peek angle in degrees (default: 14) */ peekAngle?: number; /** Total turn angle in degrees (default: 180) */ turnAngle?: number; /** Transition flip animation duration in seconds (default: 0.65) */ duration?: number; /** Easing curve for flip animation (default: "cubic-bezier(0.4, 0, 0.2, 1)") */ easing?: string; /** Shadow intensity factor (0.0 to 1.0, default: 0.45) */ shadowIntensity?: number; /** Dynamically shift spine horizontally when book is open to center the 2-page spread (default: true) */ spineShift?: boolean; /** Border radius for pages (default: "10px") */ radius?: string | number; /** Enable page numbering tags (default: true) */ showPageNumbers?: boolean; /** Enable outer book leather spine binding (default: true) */ showSpineBinding?: boolean; /** Accent glow color for active elements (default: "#00F5FF") */ accentColor?: string; /** Enable automatic page flipping (default: false) */ autoplay?: boolean; /** Autoplay interval in milliseconds (default: 3500) */ autoplayInterval?: number; /** Pause autoplay on hover (default: true) */ pauseOnHover?: boolean; /** Enable interactive click on pages to flip (default: true) */ interactive?: boolean; /** Enable navigation buttons (default: true) */ showControls?: boolean; /** Optional container class name */ className?: string; /** Optional container inline style */ style?: CSSProperties; } export interface ThreeDImagePageflipHandle { next: () => void; prev: () => void; reset: () => void; goTo: (index: number) => void; getTurnedCount: () => number; getTotalLeaves: () => number; } const DEFAULT_PAGES: PageFlipLeaf[] = [ { id: 1, frontImage: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=800&q=80", backImage: "https://images.unsplash.com/photo-1512917774080-9991f1c4c750?auto=format&fit=crop&w=800&q=80", frontTitle: "Villa Solarium", frontSubtitle: "Horizon Pool & Architecture", frontBadge: "Cover", backTitle: "Minimal Horizon", backSubtitle: "Geometric Water Pavilion", backBadge: "Plate 01", }, { id: 2, frontImage: "https://images.unsplash.com/photo-1513694203232-719a280e022f?auto=format&fit=crop&w=800&q=80", backImage: "https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=800&q=80", frontTitle: "Apex Structure", frontSubtitle: "Parametric Glass Facade", frontBadge: "Plate 02", backTitle: "Glass Skyline", backSubtitle: "Monolith Metropolitan Tower", backBadge: "Plate 03", }, { id: 3, frontImage: "https://images.unsplash.com/photo-1506744038136-46273834b3fb?auto=format&fit=crop&w=800&q=80", backImage: "https://images.unsplash.com/photo-1511818966892-d7d671e672a2?auto=format&fit=crop&w=800&q=80", frontTitle: "Emerald Cascade", frontSubtitle: "Alpine Mist & Forest Ridge", frontBadge: "Plate 04", backTitle: "Nordic Pavilion", backSubtitle: "Natural Timber Canopy", backBadge: "Plate 05", }, { id: 4, frontImage: "https://images.unsplash.com/photo-1518780664697-55e3ad937233?auto=format&fit=crop&w=800&q=80", backImage: "https://images.unsplash.com/photo-1507652313519-d4e9174996dd?auto=format&fit=crop&w=800&q=80", frontTitle: "Warm Solarium", frontSubtitle: "Sunlight & Interior Loft", frontBadge: "Plate 06", backTitle: "Zen Courtyard", backSubtitle: "Brutalist Stone Water Feature", backBadge: "Plate 07", }, { id: 5, frontImage: "https://images.unsplash.com/photo-1486325212027-8081e485255e?auto=format&fit=crop&w=800&q=80", backImage: "https://images.unsplash.com/photo-1479839672679-a46483c0e7c8?auto=format&fit=crop&w=800&q=80", frontTitle: "Cyber Metropolis", frontSubtitle: "Urban Geometric Skyline", frontBadge: "Plate 08", backTitle: "Monolith Curve", backSubtitle: "Brutalist Concrete Finish", backBadge: "Endplate", }, ]; export const ThreeDImagePageflip = forwardRef(({ pages = DEFAULT_PAGES, defaultTurnedIndex = 0, turnedIndex: controlledTurnedIndex, onPageChange, pageWidth = 230, pageHeight = 330, perspective = 1300, peekAngle = 14, turnAngle = 180, duration = 0.65, easing = "cubic-bezier(0.4, 0, 0.2, 1)", shadowIntensity = 0.45, spineShift = true, radius = "10px", showPageNumbers = true, showSpineBinding = true, accentColor = "#00F5FF", autoplay = false, autoplayInterval = 3500, pauseOnHover = true, interactive = true, showControls = true, className, style, }, ref) => { const [internalTurned, setInternalTurned] = useState(defaultTurnedIndex); const [isHovered, setIsHovered] = useState(false); const [peekingIndex, setPeekingIndex] = useState(null); const totalLeaves = pages.length; const currentTurned = controlledTurnedIndex !== undefined ? controlledTurnedIndex : internalTurned; const isOpen = currentTurned > 0 && currentTurned < totalLeaves; const parsedRadius = typeof radius === "number" ? `${radius}px` : radius; const setTurned = useCallback((newCount: number) => { const clamped = Math.max(0, Math.min(newCount, totalLeaves)); if (controlledTurnedIndex === undefined) { setInternalTurned(clamped); } if (onPageChange) { onPageChange(clamped, totalLeaves); } }, [controlledTurnedIndex, totalLeaves, onPageChange]); const flipNext = useCallback(() => { if (currentTurned < totalLeaves) { setTurned(currentTurned + 1); } }, [currentTurned, totalLeaves, setTurned]); const flipPrev = useCallback(() => { if (currentTurned > 0) { setTurned(currentTurned - 1); } }, [currentTurned, setTurned]); const resetBook = useCallback(() => { setTurned(0); }, [setTurned]); useImperativeHandle(ref, () => ({ next: flipNext, prev: flipPrev, reset: resetBook, goTo: (idx) => setTurned(idx), getTurnedCount: () => currentTurned, getTotalLeaves: () => totalLeaves, })); // Autoplay Timer useEffect(() => { if (!autoplay || (pauseOnHover && isHovered) || totalLeaves <= 1) return; const timer = setInterval(() => { setInternalTurned((prev) => (prev >= totalLeaves ? 0 : prev + 1)); }, autoplayInterval); return () => clearInterval(timer); }, [autoplay, autoplayInterval, pauseOnHover, isHovered, totalLeaves]); // Keyboard Navigation useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "ArrowRight") flipNext(); if (e.key === "ArrowLeft") flipPrev(); }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [flipNext, flipPrev]); const handleLeafClick = (index: number) => { if (!interactive) return; if (index === currentTurned) { // Click unturned top page -> flip forward flipNext(); } else if (index === currentTurned - 1) { // Click turned top left page -> flip backward flipPrev(); } }; return (
setIsHovered(true)} onMouseLeave={() => { setIsHovered(false); setPeekingIndex(null); }} > {/* 3D Book Viewport Stage */}
{/* 3D Book Container */}
{/* Spine Shadow & Binding Crease */} {showSpineBinding && (
)} {/* Ground Ambience Drop Shadow underneath the book */}
{/* Book Leaves Stacking Loop */} {pages.map((leaf, index) => { const isTurned = index < currentTurned; const isCanPeek = index === currentTurned; const isPeeking = peekingIndex === index; // Calculate Z-Index: turned leaves stack forward on left, unturned leaves stack backward on right const zIndex = isTurned ? index + 1 : totalLeaves - index; // Rotation Angle let leafRotation = isTurned ? -turnAngle : 0; if (!isTurned && isPeeking) { leafRotation = -peekAngle; } return (
handleLeafClick(index)} onMouseEnter={() => { if (isCanPeek) setPeekingIndex(index); }} onMouseLeave={() => { if (peekingIndex === index) setPeekingIndex(null); }} className={cn( "absolute inset-0 origin-left cursor-pointer", interactive ? "cursor-pointer" : "pointer-events-none" )} style={{ transformStyle: "preserve-3d", transition: `transform ${duration}s ${easing}`, transform: `rotateY(${leafRotation}deg)`, zIndex, borderRadius: parsedRadius, }} > {/* FRONT FACE (Visible when page is on the right) */}
{leaf.frontTitle {/* Spine crease shadow overlay for 3D depth */}
{/* Bottom Vignette & Metadata */}
{leaf.frontBadge && ( {leaf.frontBadge} )} {showPageNumbers && ( {index * 2 + 1} )}
{leaf.frontTitle && (

{leaf.frontTitle}

)} {leaf.frontSubtitle && (

{leaf.frontSubtitle}

)}
{/* BACK FACE (Visible when page is turned to the left) */}
{leaf.backTitle {/* Spine crease shadow overlay for turned back-face */}
{/* Bottom Vignette & Metadata */}
{leaf.backBadge && ( {leaf.backBadge} )} {showPageNumbers && ( {index * 2 + 2} )}
{leaf.backTitle && (

{leaf.backTitle}

)} {leaf.backSubtitle && (

{leaf.backSubtitle}

)}
); })}
{/* Book Controls & Page Progress Toolbar */} {showControls && (
{currentTurned} / {totalLeaves} leaves
)}
); }); ThreeDImagePageflip.displayName = "ThreeDImagePageflip"; export default ThreeDImagePageflip; ``` ## Usage ```tsx import ThreeDImagePageflip, { PageFlipLeaf } from "@/components/lightswind/3d-image-pageflip"; ``` ```tsx import ThreeDImagePageflip, { PageFlipLeaf } from "@/components/lightswind/3d-image-pageflip"; const pages: PageFlipLeaf[] = [ { frontImage: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=800&q=80", backImage: "https://images.unsplash.com/photo-1512917774080-9991f1c4c750?auto=format&fit=crop&w=800&q=80", frontTitle: "Villa Solarium", frontSubtitle: "Horizon Pool & Architecture", frontBadge: "Cover", backTitle: "Minimal Horizon", backSubtitle: "Geometric Water Pavilion", backBadge: "Plate 01", }, { frontImage: "https://images.unsplash.com/photo-1513694203232-719a280e022f?auto=format&fit=crop&w=800&q=80", backImage: "https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=800&q=80", frontTitle: "Apex Structure", frontSubtitle: "Parametric Glass Facade", frontBadge: "Plate 02", backTitle: "Glass Skyline", backSubtitle: "Monolith Tower", backBadge: "Plate 03", }, ]; export default function Example() { return (
); } ```