Flipbook Codepen Info

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>Flipbook Animation | Interactive Canvas Flipbook</title>
    <style>
        * 
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            user-select: none; /* Prevent accidental selection while dragging */
body 
            background: linear-gradient(145deg, #2c3e50 0%, #1a2632 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: 'Segoe UI', 'Poppins', 'Inter', system-ui, -apple-system, sans-serif;
            padding: 20px;
/* Main flipbook card container */
        .flipbook-container 
            background: rgba(0, 0, 0, 0.35);
            border-radius: 48px;
            padding: 24px 20px 28px 20px;
            backdrop-filter: blur(2px);
            box-shadow: 0 25px 45px rgba(0,0,0,0.3), inset 0 1px 1px rgba(255,255,255,0.1);
.flipbook 
            position: relative;
            width: 600px;
            height: 400px;
            background: #fef9e8;
            border-radius: 18px;
            box-shadow: 0 30px 40px -15px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,245,215,0.5) inset;
            cursor: grab;
            overflow: hidden;
            transition: box-shadow 0.2s;
.flipbook:active 
            cursor: grabbing;
canvas 
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            display: block;
            border-radius: 16px;
            pointer-events: none;  /* We handle mouse events on wrapper */
/* Control panel */
        .controls 
            display: flex;
            justify-content: center;
            align-items: center;
            gap: 18px;
            margin-top: 28px;
            flex-wrap: wrap;
button 
            background: #1e2a36;
            border: none;
            color: #ffecb3;
            font-size: 1.35rem;
            font-weight: 600;
            padding: 10px 24px;
            border-radius: 60px;
            cursor: pointer;
            transition: 0.2s ease;
            box-shadow: 0 5px 0 #0f1419;
            font-family: inherit;
            letter-spacing: 0.5px;
            display: inline-flex;
            align-items: center;
            gap: 10px;
button:active 
            transform: translateY(2px);
            box-shadow: 0 2px 0 #0f1419;
button i 
            font-style: normal;
            font-weight: bold;
            font-size: 1.2rem;
.page-indicator 
            background: #00000066;
            backdrop-filter: blur(12px);
            padding: 8px 20px;
            border-radius: 60px;
            color: white;
            font-weight: 600;
            font-size: 1.2rem;
            letter-spacing: 1px;
            font-family: monospace;
            box-shadow: inset 0 0 2px rgba(255,255,200,0.6), 0 4px 12px rgba(0,0,0,0.2);
.progress-slider 
            display: flex;
            align-items: center;
            gap: 14px;
            background: #1e2a36aa;
            padding: 5px 18px;
            border-radius: 60px;
            backdrop-filter: blur(8px);
.progress-slider label 
            color: #ffe6b3;
            font-weight: 500;
input[type="range"] 
            width: 200px;
            height: 4px;
            -webkit-appearance: none;
            background: #cfb284;
            border-radius: 5px;
            outline: none;
input[type="range"]:focus 
            outline: none;
input[type="range"]::-webkit-slider-thumb 
            -webkit-appearance: none;
            width: 16px;
            height: 16px;
            border-radius: 50%;
            background: #ffdd99;
            cursor: pointer;
            border: none;
            box-shadow: 0 1px 4px black;
@media (max-width: 680px) 
            .flipbook 
                width: 90vw;
                height: calc(90vw * 0.666);
.controls 
                gap: 12px;
button 
                padding: 6px 16px;
                font-size: 1rem;
.progress-slider input 
                width: 130px;
.footer-note 
            text-align: center;
            margin-top: 20px;
            font-size: 0.75rem;
            color: #d9cba3;
            font-weight: 500;
            letter-spacing: 0.5px;
.badge 
            background: #00000055;
            display: inline-block;
            padding: 3px 12px;
            border-radius: 40px;
</style>
</head>
<body>
<div>
    <div class="flipbook-container">
        <div class="flipbook" id="flipbookWrapper">
            <canvas id="flipCanvas" width="600" height="400"></canvas>
        </div>
<div class="controls">
            <button id="prevBtn" aria-label="Previous page">◀ PREV</button>
            <div class="page-indicator" id="pageIndicator">PAGE 1 / 12</div>
            <button id="nextBtn" aria-label="Next page">NEXT ▶</button>
            <div class="progress-slider">
                <label>📖</label>
                <input type="range" id="pageSlider" min="1" max="12" step="1" value="1">
            </div>
        </div>
        <div class="footer-note">
            <span class="badge">✨ Drag horizontally to flip pages • Interactive flipbook ✨</span>
        </div>
    </div>
</div>
<script>
    (function()
        // ---- FLIPBOOK CONFIGURATION ----
        const TOTAL_PAGES = 12;     // 12 unique illustrated pages
        let currentPage = 1;        // 1-indexed
// Canvas references
        const canvas = document.getElementById('flipCanvas');
        const ctx = canvas.getContext('2d');
        const wrapper = document.getElementById('flipbookWrapper');
// UI elements
        const prevBtn = document.getElementById('prevBtn');
        const nextBtn = document.getElementById('nextBtn');
        const pageIndicator = document.getElementById('pageIndicator');
        const pageSlider = document.getElementById('pageSlider');
// Drag-to-flip variables
        let isDragging = false;
        let dragStartX = 0;
        let dragThreshold = 50;      // minimum horizontal drag to flip page (px)
// Optional: for smooth transitions we can use flag to avoid multiple flips during drag
        let dragProcessed = false;
// ---- DRAWING ENGINE: each page gets a unique artistic theme / flipbook story ----
        // All pages drawn dynamically with colorful vector-style illustrations.
        // Story theme: "Cosmic Journey of a Curious Cat"
        function drawPage(pageNumber)
// ----- individual drawing helpers (mini vector art) -----
        function drawMoonCat(ctx, w, h) 
            ctx.save();
            ctx.shadowBlur = 0;
            ctx.beginPath();
            ctx.arc(w*0.7, h*0.65, w*0.09, 0, Math.PI*2);
            ctx.fillStyle = '#FFE6B0';
            ctx.fill();
            ctx.fillStyle = '#4a3727';
            ctx.beginPath();
            ctx.ellipse(w*0.66, h*0.62, w*0.02, h*0.03, 0, 0, Math.PI*2);
            ctx.fill();
            ctx.beginPath();
            ctx.ellipse(w*0.74, h*0.62, w*0.02, h*0.03, 0, 0, Math.PI*2);
            ctx.fill();
            ctx.beginPath();
            ctx.arc(w*0.7, h*0.68, w*0.03, 0, Math.PI);
            ctx.fill();
            // ears
            ctx.fillStyle = '#E5BE8F';
            ctx.beginPath(); ctx.moveTo(w*0.63, h*0.57); ctx.lineTo(w*0.60, h*0.51); ctx.lineTo(w*0.67, h*0.56); ctx.fill();
            ctx.beginPath(); ctx.moveTo(w*0.77, h*0.57); ctx.lineTo(w*0.80, h*0.51); ctx.lineTo(w*0.73, h*0.56); ctx.fill();
            // moon
            ctx.fillStyle = '#F5E7A3';
            ctx.beginPath(); ctx.arc(w*0.3, h*0.3, w*0.08, 0, Math.PI*2); ctx.fill();
            ctx.fillStyle = '#E9CF7A';
            ctx.beginPath(); ctx.arc(w*0.28, h*0.27, w*0.06, 0, Math.PI*2); ctx.fill();
            ctx.restore();
function drawKite(ctx, w, h) 
            ctx.fillStyle = '#ffaa66'; ctx.beginPath(); ctx.moveTo(w*0.7, h*0.5); ctx.lineTo(w*0.8, h*0.6); ctx.lineTo(w*0.7, h*0.7); ctx.lineTo(w*0.6, h*0.6); ctx.fill();
            ctx.fillStyle = '#dd8844'; ctx.beginPath(); ctx.moveTo(w*0.7, h*0.5); ctx.lineTo(w*0.7, h*0.3); ctx.lineTo(w*0.78, h*0.45); ctx.fill();
            ctx.beginPath(); ctx.moveTo(w*0.7, h*0.7); ctx.lineTo(w*0.7, h*0.85); ctx.lineWidth=3; ctx.strokeStyle='#b97f44'; ctx.stroke();
function drawStars(ctx, w, h)  for(let i=0;i<12;i++) ctx.fillStyle=`hsl($40+i*20, 80%, 65%)`; ctx.beginPath(); ctx.arc(w*(0.2+Math.sin(i)*0.1), h*(0.5+Math.cos(i*2)*0.2), w*0.02,0,Math.PI*2); ctx.fill();
        function drawRainbow(ctx,w,h) for(let i=0;i<6;i++) ctx.fillStyle=`hsl($30+i*15, 80%, 65%)`; ctx.fillRect(w*0.2, h*0.55 + i*12, w*0.6, 8);  
        function drawClouds(ctx,w,h) ctx.fillStyle='#F0F8FF'; ctx.beginPath(); ctx.ellipse(w*0.3,h*0.7,w*0.12,w*0.08,0,0,Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.ellipse(w*0.45,h*0.68,w*0.1,w*0.07,0,0,Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.ellipse(w*0.6,h*0.72,w*0.13,w*0.09,0,0,Math.PI*2); ctx.fill();
        function drawGalaxy(ctx,w,h) for(let s=0;s<60;s++) ctx.fillStyle=`rgba(180,130,255,$Math.random()*0.6)`; ctx.fillRect(w*0.65+Math.random()*80, h*0.4+Math.random()*80, 2,2);  ctx.fillStyle='#c7aaff'; ctx.beginPath(); ctx.ellipse(w*0.75,h*0.65,w*0.08,w*0.04,0,0,Math.PI*2); ctx.fill();
        function drawTeaParty(ctx,w,h) ctx.fillStyle='#d9b48b'; ctx.fillRect(w*0.55,h*0.6,w*0.12,w*0.1); ctx.fillStyle='#f3e3c2'; ctx.beginPath(); ctx.ellipse(w*0.61,h*0.58,w*0.07,w*0.04,0,0,Math.PI*2); ctx.fill(); ctx.fillStyle='#a57c54'; ctx.fillRect(w*0.6,h*0.7,3,12);
        function drawMoonCrater(ctx,w,h) ctx.fillStyle='#cbc1a4'; ctx.beginPath(); ctx.arc(w*0.7, h*0.6, w*0.1,0,Math.PI*2); ctx.fill(); ctx.fillStyle='#a59173'; ctx.beginPath(); ctx.ellipse(w*0.72, h*0.58, w*0.03, w*0.02,0,0,Math.PI*2); ctx.fill();
        function drawConstellation(ctx,w,h) ctx.beginPath(); for(let i=0;i<5;i++) let x = w*(0.6+Math.sin(i)*0.08); let y = h*(0.5+Math.cos(i*2)*0.08); ctx.fillStyle='#ffd966'; ctx.arc(x,y,4,0,Math.PI*2); ctx.fill(); ctx.fillStyle='gold'; ctx.fill(); if(i>0) ctx.fillRect(x-2,y-2,4,4);  
        function drawShootingStar(ctx,w,h) ctx.fillStyle='#FFE484'; ctx.beginPath(); ctx.moveTo(w*0.8,h*0.3); ctx.lineTo(w*0.83,h*0.25); ctx.lineTo(w*0.75,h*0.28); ctx.fill(); ctx.fillStyle='white'; for(let i=0;i<8;i++) ctx.fillRect(w*0.7+Math.random()*40, h*0.25+Math.random()*30, 2,2);
        function drawNebula(ctx,w,h) ctx.globalAlpha=0.5; for(let i=0;i<40;i++) ctx.fillStyle=`hsl($280+Math.random()*40, 80%, 70%)`; ctx.beginPath(); ctx.arc(w*(0.65+Math.random()*0.3), h*(0.5+Math.random()*0.3), Math.random()*8,0,Math.PI*2); ctx.fill();  ctx.globalAlpha=1;
        function drawHomecoming(ctx,w,h) ctx.fillStyle='#78b57e'; ctx.fillRect(w*0.2,h*0.7,w*0.6,15); ctx.fillStyle='#6c9e6e'; ctx.beginPath(); ctx.rect(w*0.35,h*0.5,w*0.1,w*0.2); ctx.fill(); ctx.fillStyle='#b57c48'; ctx.beginPath(); ctx.moveTo(w*0.32,h*0.5); ctx.lineTo(w*0.4,h*0.42); ctx.lineTo(w*0.48,h*0.5); ctx.fill();
// ----- update UI and canvas -----
        function updateFlipbook() 
            drawPage(currentPage);
            pageIndicator.innerText = `PAGE $currentPage / $TOTAL_PAGES`;
            pageSlider.value = currentPage;
            // update button disabled states (optional style)
            prevBtn.disabled = (currentPage === 1);
            nextBtn.disabled = (currentPage === TOTAL_PAGES);
            prevBtn.style.opacity = (currentPage === 1) ? "0.5" : "1";
            nextBtn.style.opacity = (currentPage === TOTAL_PAGES) ? "0.5" : "1";
function nextPage() 
            if(currentPage < TOTAL_PAGES) 
                currentPage++;
                updateFlipbook();
function prevPage() 
            if(currentPage > 1) 
                currentPage--;
                updateFlipbook();
// ----- Drag to flip (natural flipbook interaction) -----
        function onDragStart(e) 
            e.preventDefault();
            isDragging = true;
            dragProcessed = false;
            const clientX = e.type.includes('mouse') ? e.clientX : (e.touches ? e.touches[0].clientX : e.clientX);
            dragStartX = clientX;
            wrapper.style.cursor = 'grabbing';
function onDragMove(e) 
            if(!isDragging) return;
            e.preventDefault();
            let currentX = e.type.includes('mouse') ? e.clientX : (e.touches ? e.touches[0].clientX : e.clientX);
            let deltaX = currentX - dragStartX;
            // threshold flip detection
            if(!dragProcessed && Math.abs(deltaX) > dragThreshold) 
                if(deltaX > 0) 
                    // drag right -> previous page
                    if(currentPage > 1) 
                        prevPage();
                        dragProcessed = true;
                     else dragProcessed = true;
                 else if(deltaX < 0) 
                    // drag left -> next page
                    if(currentPage < TOTAL_PAGES) 
                        nextPage();
                        dragProcessed = true;
                     else dragProcessed = true;
// reset drag after flip to avoid multiple flips per gesture
                setTimeout(() => 
                    if(isDragging) 
                        isDragging = false;
                        wrapper.style.cursor = 'grab';
, 50);
                isDragging = false;
                wrapper.style.cursor = 'grab';
function onDragEnd(e) 
            isDragging = false;
            dragProcessed = false;
            wrapper.style.cursor = 'grab';
// ----- event binding for mouse + touch -----
        function bindDragEvents() 
            wrapper.addEventListener('mousedown', onDragStart);
            window.addEventListener('mousemove', onDragMove);
            window.addEventListener('mouseup', onDragEnd);
wrapper.addEventListener('touchstart', onDragStart, passive: false);
            window.addEventListener('touchmove', onDragMove, passive: false);
            window.addEventListener('touchend', onDragEnd);
// init slider & buttons
        function bindControls() 
            prevBtn.addEventListener('click', prevPage);
            nextBtn.addEventListener('click', nextPage);
            pageSlider.addEventListener('input', (e) => 
                const val = parseInt(e.target.value, 10);
                if(!isNaN(val) && val >=1 && val <= TOTAL_PAGES && val !== currentPage)
                    currentPage = val;
                    updateFlipbook();
);
// resize observer for canvas crispness (fixed size but ensures ratio)
        function handleResize() 
            const rect = wrapper.getBoundingClientRect();
            canvas.width = rect.width;
            canvas.height = rect.height;
            updateFlipbook();
const resizeObserver = new ResizeObserver(() =>  handleResize(); );
        resizeObserver.observe(wrapper);
        handleResize();
bindDragEvents();
        bindControls();
        updateFlipbook();
// initial canvas draw fix after load
        window.addEventListener('load', () => 
            handleResize();
        );
    )();
</script>
</body>
</html>

Creating a flipbook on is a great way to showcase digital content with a tactile, interactive feel. You can build one from scratch using HTML/CSS or use a library like for more advanced features 1. The "From Scratch" Method (CSS 3D Transforms)

This method uses pure CSS to handle the 3D flipping effect. It’s lightweight and great for learning how 3D space works in the browser. Structure (HTML):

Create a container for the "book" and nested divs for each "page." Each page should have a "front" and "back" face. Perspective (CSS): perspective: 1000px; to the book container to give it depth. Use transform-style: preserve-3d; on the pages so their children exist in 3D space. The Flip (JS): Use a simple event listener to toggle a class. In your CSS, define that class to rotate the page: transform: rotateY(-180deg); 2. The Library Method (Turn.js)

If you want professional features like "peeling" corners or realistic shadows, using a library is much faster. In your CodePen settings, add the CDN links to the "JS" tab. Initialization: Wrap your pages in a single ). In your JS panel, initialize it with a single line: javascript "#flipbook" ).turn({ width: , autoCenter: Use code with caution. Copied to clipboard Responsive Design:

Use a wrapper that centers the book vertically and horizontally to ensure it looks good on all screen sizes. 3. Quick PDF Embed Method

If you already have a PDF and just want to display it as a flipbook without coding the logic: Iframe Shortcut: You can use services like to host your PDF and embed it on CodePen via an iframe. Code Example: "https://flowpaper.com" Use code with caution. Copied to clipboard Best Practices for Digital Flipbooks Z-Index Management:

When flipping pages, ensure the "active" page has the highest so it doesn't clip through other pages. Performance: flipbook codepen

Avoid using overly large, unoptimized images, as they can cause the "flip" animation to lag. Backface Visibility: Always set backface-visibility: hidden;

on your page faces to prevent the content of the "back" from showing through the "front" during the rotation. to copy directly into a new CodePen?

A flipbook effect simulates turning physical pages in a book or magazine. It’s used for digital magazines, portfolios, product catalogs, onboarding tutorials, and interactive storytelling. The key illusion: a page appears to rotate or curl while revealing content on the reverse side.

Why implement on CodePen?


In the CSS, look for transition: transform 0.6s ease. Change 0.6s to 0.3s for a snappier, modern e-reader feel. Increase it to 1s for a luxurious, slow-motion magazine flip.

To change page thickness (the "stack" visual), look for box-shadow on the .page element. Creating a flipbook on is a great way

Search tag: three js flipbook codepen For the bleeding edge, WebGL flipbooks render actual 3D meshes of paper. You can look at the book from any angle. These are resource-intensive but mind-blowing.

Best for: Architectural visualization, 3D portfolios, luxury brand lookbooks.

Here is a clean, modern implementation of a CSS 3D Flipbook that you can drop into your project. This does not require any external libraries.

For simplicity, we’ll generate colored circles that “move” across frames. In a real flipbook, you could load sprite sheets or draw SVG paths.

const canvas = document.getElementById('flipbookCanvas');
const ctx = canvas.getContext('2d');
const totalFrames = 12;
let currentFrame = 0;
let frames = [];

// Generate frames: a bouncing circle for (let i = 0; i < totalFrames; i++) let tempCanvas = document.createElement('canvas'); tempCanvas.width = 400; tempCanvas.height = 400; let tempCtx = tempCanvas.getContext('2d');

tempCtx.fillStyle = '#fdf6e3'; tempCtx.fillRect(0, 0, 400, 400); In the CSS, look for transition: transform 0

// Circle moves from left to right let x = 50 + (i / (totalFrames - 1)) * 300; let y = 200 + 50 * Math.sin(i * Math.PI / 4);

tempCtx.beginPath(); tempCtx.arc(x, y, 30, 0, Math.PI * 2); tempCtx.fillStyle = '#d23669'; tempCtx.fill(); tempCtx.fillStyle = '#000'; tempCtx.font = 'bold 20px monospace'; tempCtx.fillText(i+1, x-10, y-15);

frames.push(tempCanvas);

function drawFrame(index) ctx.clearRect(0, 0, 400, 400); ctx.drawImage(frames[index], 0, 0); document.getElementById('pageNum').innerText = Page $index+1 / $totalFrames; document.getElementById('slider').value = index;

drawFrame(0);