Flipbook Codepen May 2026
<!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 Canvas | Interactive Draggable Animation</title>
<style>
*
user-select: none; /* Prevent accidental text selection while dragging */
-webkit-tap-highlight-color: transparent;
body
background: linear-gradient(145deg, #1a2a3a 0%, #0f1a24 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', 'Poppins', 'Courier New', monospace;
margin: 0;
padding: 20px;
/* Card container with soft shadow */
.flipbook-container
background: rgba(30, 40, 50, 0.6);
border-radius: 48px;
padding: 24px 20px 20px 20px;
box-shadow: 0 25px 45px rgba(0, 0, 0, 0.4), inset 0 1px 2px rgba(255, 255, 255, 0.1);
backdrop-filter: blur(2px);
canvas
display: block;
margin: 0 auto;
border-radius: 20px;
box-shadow: 0 20px 35px rgba(0, 0, 0, 0.4), 0 0 0 8px #f9e6cf, 0 0 0 12px #c9aa7b;
cursor: grab;
background: #fef0da;
transition: box-shadow 0.1s ease;
canvas:active
cursor: grabbing;
/* Control panel — retro & playful */
.controls
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 18px;
margin-top: 28px;
margin-bottom: 12px;
button
background: #2c3e2f;
border: none;
font-size: 1.4rem;
font-weight: bold;
font-family: monospace;
padding: 8px 18px;
border-radius: 60px;
color: #ffe7c4;
cursor: pointer;
box-shadow: 0 5px 0 #1a2a1d;
transition: 0.07s linear;
letter-spacing: 1px;
backdrop-filter: blur(4px);
background: #3b4e3e;
button:active
transform: translateY(3px);
box-shadow: 0 2px 0 #1a2a1d;
.page-indicator
background: #1e2a24d9;
backdrop-filter: blur(12px);
padding: 6px 22px;
border-radius: 100px;
font-weight: bold;
font-size: 1.3rem;
color: #fae6b3;
font-family: 'Courier New', monospace;
display: inline-flex;
align-items: center;
gap: 12px;
box-shadow: inset 0 1px 3px #00000033, 0 5px 10px #0000002e;
.indicator-value
background: #00000055;
padding: 2px 14px;
border-radius: 50px;
font-size: 1.6rem;
min-width: 70px;
text-align: center;
font-weight: 800;
color: #ffdd99;
.drag-hint
text-align: center;
margin-top: 16px;
font-size: 0.85rem;
font-family: monospace;
color: #c0cfb5;
background: #00000055;
display: inline-block;
width: auto;
padding: 6px 16px;
border-radius: 50px;
backdrop-filter: blur(4px);
.footer
display: flex;
justify-content: center;
gap: 20px;
align-items: center;
flex-wrap: wrap;
@media (max-width: 550px)
.flipbook-container
padding: 16px;
button
padding: 5px 14px;
font-size: 1rem;
.indicator-value
font-size: 1.2rem;
min-width: 55px;
</style>
</head>
<body>
<div>
<div class="flipbook-container">
<canvas id="flipCanvas" width="600" height="400" style="width:100%; height:auto; max-width:600px; aspect-ratio:600/400"></canvas>
<div class="controls">
<button id="prevBtn" aria-label="previous page">◀ PREV</button>
<div class="page-indicator">
📖 PAGE <span id="pageNum" class="indicator-value">1</span> / <span id="totalPages">12</span>
</div>
<button id="nextBtn" aria-label="next page">NEXT ▶</button>
</div>
<div class="footer">
<div class="drag-hint">✋ DRAG HORIZONTALLY → flip pages like a real book</div>
</div>
</div>
</div>
<script>
(function()
// ----- CONFIGURATION -----
const canvas = document.getElementById('flipCanvas');
const ctx = canvas.getContext('2d');
// Flipbook settings
const TOTAL_PAGES = 12; // 12 pages total (6 spreads / 12 individual views)
let currentPage = 1; // 1-indexed page number (1 to TOTAL_PAGES)
// Dragging state
let isDragging = false;
let dragStartX = 0;
let dragThreshold = 25; // minimum drag distance to flip page (pixels)
// Animation / smoothing for page turns (optional subtle effect)
let transitionOffset = 0; // not used for persistent drag, just for UX feedback
let flipInProgress = false;
// ----- DRAWING ENGINE: each page has a unique vibrant illustration -----
// All drawing is based on the current page number. Each page is handcrafted with retro flipbook energy.
function drawPage(pageNumber)
if (!ctx) return;
// clear canvas with warm paper texture
ctx.clearRect(0, 0, canvas.width, canvas.height);
// background: vintage paper effect (soft grain)
const grad = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
grad.addColorStop(0, '#fff6e8');
grad.addColorStop(1, '#faeecd');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// subtle grid / flipbook texture
ctx.save();
ctx.globalAlpha = 0.12;
for(let i = 0; i < canvas.width; i += 20)
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i, canvas.height);
ctx.strokeStyle = '#aa8c54';
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, i);
ctx.lineTo(canvas.width, i);
ctx.stroke();
ctx.restore();
// decorative border reminiscent of flipbook frames
ctx.strokeStyle = '#cb9e6b';
ctx.lineWidth = 8;
ctx.strokeRect(12, 12, canvas.width - 24, canvas.height - 24);
ctx.strokeStyle = '#e5c8a3';
ctx.lineWidth = 2;
ctx.strokeRect(18, 18, canvas.width - 36, canvas.height - 36);
// draw the unique page content based on page number
// each page delivers a distinct sketch / icon and a fun fact or short phrase
ctx.font = `bold $Math.floor(canvas.height * 0.09)px "Courier New", monospace`;
ctx.fillStyle = '#4e3b28';
ctx.shadowBlur = 0;
// page header: small flipbook indicator
ctx.font = `12px monospace`;
ctx.fillStyle = '#b48b5a';
ctx.fillText(`✦ page $pageNumber ✦`, canvas.width - 70, 35);
ctx.fillStyle = '#7c5e3c';
ctx.fillText(`flip·book`, 20, 35);
// main content style
ctx.font = `500 $Math.floor(canvas.height * 0.06)px "Segoe UI", "Courier New", monospace`;
ctx.fillStyle = '#3a2c1e';
ctx.shadowBlur = 0;
// ---- creative illustration per page ----
// Center coordinates for icon
const centerX = canvas.width/2;
const centerY = canvas.height/2 - 10;
const iconSize = Math.min(canvas.width * 0.2, 80);
// draw each page uniquely
switch(pageNumber)
case 1:
drawStickFigure(centerX, centerY, iconSize);
ctx.fillText("✨ THE BEGINNING", centerX-70, centerY+50);
ctx.font = `14px monospace`;
ctx.fillStyle = '#6e583f';
ctx.fillText("Every story starts with a flip", centerX-100, centerY+95);
break;
case 2:
drawSunburst(centerX, centerY, iconSize);
ctx.fillText("☀️ SUNRISE", centerX-55, centerY+55);
ctx.font = `italic 14px monospace`;
ctx.fillText("morning glow", centerX-45, centerY+95);
break;
case 3:
drawWave(centerX, centerY, iconSize);
ctx.fillText("🌊 OCEAN WAVE", centerX-70, centerY+55);
ctx.fillText("endless motion", centerX-60, centerY+95);
break;
case 4:
drawTree(centerX, centerY, iconSize);
ctx.fillText("🌳 OAK TREE", centerX-60, centerY+55);
ctx.fillText("roots & leaves", centerX-55, centerY+95);
break;
case 5:
drawStar(centerX, centerY, iconSize);
ctx.fillText("⭐ SHOOTING STAR", centerX-85, centerY+55);
ctx.fillText("make a wish", centerX-55, centerY+95);
break;
case 6:
drawHeart(centerX, centerY, iconSize);
ctx.fillText("❤️ HEARTBEAT", centerX-68, centerY+55);
ctx.fillStyle = "#8b3c3c";
ctx.fillText("thump thump", centerX-50, centerY+95);
break;
case 7:
drawRocket(centerX, centerY, iconSize);
ctx.fillText("🚀 TO THE MOON", centerX-80, centerY+55);
ctx.fillText("adventure awaits", centerX-70, centerY+95);
break;
case 8:
drawButterfly(centerX, centerY, iconSize);
ctx.fillText("🦋 METAMORPHOSIS", centerX-95, centerY+55);
ctx.fillText("spread your wings", centerX-70, centerY+95);
break;
case 9:
drawCoffee(centerX, centerY, iconSize);
ctx.fillText("☕ COFFEE BREAK", centerX-80, centerY+55);
ctx.fillText("stay animated", centerX-55, centerY+95);
break;
case 10:
drawMountain(centerX, centerY, iconSize);
ctx.fillText("⛰️ PEAK", centerX-45, centerY+55);
ctx.fillText("higher every frame", centerX-70, centerY+95);
break;
case 11:
drawBookStack(centerX, centerY, iconSize);
ctx.fillText("📚 FLIPBOOK MAGIC", centerX-85, centerY+55);
ctx.fillText("pages in motion", centerX-65, centerY+95);
break;
case 12:
drawFireworks(centerX, centerY, iconSize);
ctx.fillText("🎆 THE END?", centerX-60, centerY+55);
ctx.fillText("... or just a new flip", centerX-70, centerY+95);
break;
default:
drawStickFigure(centerX, centerY, iconSize);
ctx.fillText(`page $pageNumber`, centerX-40, centerY+55);
break;
// add page curl effect: small shadow on right edge
ctx.save();
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.moveTo(canvas.width-10, 10);
ctx.quadraticCurveTo(canvas.width, 20, canvas.width-12, canvas.height-15);
ctx.lineTo(canvas.width-25, canvas.height-5);
ctx.fillStyle = '#ddc6a388';
ctx.fill();
ctx.restore();
// footnote: classic flipbook vibe
ctx.font = `9px monospace`;
ctx.fillStyle = '#a98754';
ctx.fillText("◀ drag edge to flip ▶", canvas.width-130, canvas.height-12);
// ----- helper illustration functions (minimal but expressive) -----
function drawStickFigure(x, y, size)
ctx.beginPath();
ctx.arc(x, y-size*0.2, size*0.2, 0, Math.PI*2);
ctx.fillStyle = '#4a3624';
ctx.fill();
ctx.beginPath();
ctx.moveTo(x, y-size*0.02);
ctx.lineTo(x, y+size*0.25);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x-size*0.18, y+size*0.08);
ctx.lineTo(x+size*0.18, y+size*0.08);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x, y+size*0.25);
ctx.lineTo(x-size*0.2, y+size*0.45);
ctx.moveTo(x, y+size*0.25);
ctx.lineTo(x+size*0.2, y+size*0.45);
ctx.stroke();
function drawSunburst(x,y,s)
for(let i=0;i<12;i++)
let angle = i * Math.PI*2/12;
let x2 = x+Math.cos(angle)*s*0.7;
let y2 = y+Math.sin(angle)*s*0.7;
ctx.beginPath();
ctx.moveTo(x,y);
ctx.lineTo(x2,y2);
ctx.lineWidth=5;
ctx.strokeStyle='#f7b32b';
ctx.stroke();
ctx.beginPath();
ctx.arc(x,y,s*0.25,0,Math.PI*2);
ctx.fillStyle='#ffcf40';
ctx.fill();
function drawWave(x,y,s)
ctx.beginPath();
for(let i=0;i<=4;i++)
let px = x - s*0.6 + i*(s*0.3);
let py = y + Math.sin(i*1.2)*s*0.2;
if(i===0) ctx.moveTo(px,py);
else ctx.lineTo(px,py);
ctx.strokeStyle='#2c6e9e';
ctx.lineWidth=4;
ctx.stroke();
function drawTree(x,y,s)
ctx.fillStyle = '#b87c4f';
ctx.fillRect(x-s*0.08, y-s*0.1, s*0.16, s*0.5);
ctx.fillStyle = '#5f8b4c';
ctx.beginPath();
ctx.arc(x, y-s*0.25, s*0.35, 0, Math.PI*2);
ctx.fill();
function drawStar(x,y,s)
let spikes=5;
let outer=s*0.6;
let inner=s*0.25;
let step=Math.PI/spikes;
ctx.beginPath();
for(let i=0;i<2*spikes;i++)
let r = (i%2===0)?outer:inner;
let angle = i*step - Math.PI/2;
let px = x+Math.cos(angle)*r;
let py = y+Math.sin(angle)*r;
if(i===0) ctx.moveTo(px,py);
else ctx.lineTo(px,py);
ctx.closePath();
ctx.fillStyle='#f3c26b';
ctx.fill();
function drawHeart(x,y,s)
ctx.beginPath();
let topCurve = s*0.4;
ctx.moveTo(x, y+topCurve*0.4);
ctx.bezierCurveTo(x-topCurve, y-topCurve, x-topCurve*1.2, y+topCurve*0.9, x, y+topCurve*1.1);
ctx.bezierCurveTo(x+topCurve*1.2, y+topCurve*0.9, x+topCurve, y-topCurve, x, y+topCurve*0.4);
ctx.fillStyle='#e34242';
ctx.fill();
function drawRocket(x,y,s)
ctx.fillStyle='#9f7e69';
ctx.fillRect(x-s*0.12, y-s*0.05, s*0.24, s*0.5);
ctx.beginPath();
ctx.moveTo(x-s*0.18, y+s*0.45);
ctx.lineTo(x, y+s*0.7);
ctx.lineTo(x+s*0.18, y+s*0.45);
ctx.fill();
ctx.fillStyle='#df5e2a';
ctx.beginPath();
ctx.ellipse(x, y-s*0.05, s*0.22, s*0.28, 0, 0, Math.PI*2);
ctx.fill();
function drawButterfly(x,y,s)
ctx.fillStyle='#dc9e6f';
ctx.beginPath(); ctx.ellipse(x-s*0.3, y, s*0.3, s*0.2, -0.5, 0, 2*Math.PI); ctx.fill();
ctx.beginPath(); ctx.ellipse(x+s*0.3, y, s*0.3, s*0.2, 0.5, 0, 2*Math.PI); ctx.fill();
ctx.fillStyle='#885e3e';
ctx.fillRect(x-s*0.05, y-s*0.05, s*0.1, s*0.25);
function drawCoffee(x,y,s)
ctx.fillStyle='#af7f51';
ctx.fillRect(x-s*0.2, y-s*0.2, s*0.4, s*0.45);
ctx.beginPath(); ctx.ellipse(x+s*0.25, y-s*0.02, s*0.1, s*0.18, 0, 0, 2*Math.PI); ctx.fill();
function drawMountain(x,y,s)
ctx.beginPath(); ctx.moveTo(x-s*0.5, y+s*0.2);
ctx.lineTo(x, y-s*0.3); ctx.lineTo(x+s*0.5, y+s*0.2); ctx.fillStyle='#7d9e6b'; ctx.fill();
ctx.fillStyle='white'; ctx.beginPath(); ctx.moveTo(x-s*0.1, y-s*0.05); ctx.lineTo(x, y-s*0.2); ctx.lineTo(x+s*0.1, y-s*0.05); ctx.fill();
function drawBookStack(x,y,s)
for(let i=0;i<3;i++) ctx.fillStyle = `#bd9a6$40+i*5`; ctx.fillRect(x-s*0.3+(i*4), y-s*0.2+(i*5), s*0.6, s*0.12);
ctx.fillStyle='#ab8a54'; ctx.fillRect(x-s*0.25, y-s*0.23, s*0.5, 8);
function drawFireworks(x,y,s)
for(let i=0;i<20;i++) let angle=Math.random()*6.28; let r=Math.random()*s*0.7; let x2=x+Math.cos(angle)*r; let y2=y+Math.sin(angle)*r; ctx.beginPath(); ctx.arc(x2,y2,3,0,2*Math.PI); ctx.fillStyle=`hsl($Math.random()*360,70%,60%)`; ctx.fill();
// update canvas and page indicator text
function renderCurrentPage()
drawPage(currentPage);
const pageSpan = document.getElementById('pageNum');
if(pageSpan) pageSpan.innerText = currentPage;
// navigate to a specific page safely
function goToPage(page)
if(page < 1) page = 1;
if(page > TOTAL_PAGES) page = TOTAL_PAGES;
if(currentPage === page) return;
currentPage = page;
renderCurrentPage();
function nextPage()
if(currentPage < TOTAL_PAGES)
goToPage(currentPage + 1);
else
// playful hint: add a little bounce effect to show it's the last page
canvas.style.transform = 'scale(0.99)';
setTimeout(()=> canvas.style.transform = ''; , 120);
function prevPage()
if(currentPage > 1)
goToPage(currentPage - 1);
else
canvas.style.transform = 'scale(0.99)';
setTimeout(()=> canvas.style.transform = ''; , 120);
// ----- DRAG TO FLIP LOGIC (flipbook style) -----
function onPointerStart(e)
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const clientX = e.clientX ?? (e.touches ? e.touches[0].clientX : 0);
dragStartX = (clientX - rect.left) * scaleX;
isDragging = true;
canvas.style.cursor = 'grabbing';
function onPointerMove(e)
if(!isDragging) return;
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const clientX = e.clientX ?? (e.touches ? e.touches[0].clientX : 0);
let currentDragX = (clientX - rect.left) * scaleX;
let deltaX = currentDragX - dragStartX;
// if the drag exceeds threshold, flip page and reset drag
if(Math.abs(deltaX) >= dragThreshold)
if(deltaX > 0)
// drag right -> previous page (like pulling from left edge)
if(currentPage > 1)
prevPage();
else
// drag left -> next page
if(currentPage < TOTAL_PAGES)
nextPage();
// reset drag state to avoid multiple flips per gesture
isDragging = false;
canvas.style.cursor = 'grab';
// tiny haptic feedback via transform flash
canvas.style.transform = 'scale(0.98)';
setTimeout(()=> canvas.style.transform = ''; , 100);
dragStartX = 0;
function onPointerEnd(e)
if(isDragging)
isDragging = false;
canvas.style.cursor = 'grab';
// attach both mouse and touch events
function attachDragEvents()
canvas.addEventListener('mousedown', onPointerStart);
window.addEventListener('mousemove', onPointerMove);
window.addEventListener('mouseup', onPointerEnd);
canvas.addEventListener('touchstart', onPointerStart, passive: false);
window.addEventListener('touchmove', onPointerMove, passive: false);
window.addEventListener('touchend', onPointerEnd);
canvas.addEventListener('dragstart', (e) => e.preventDefault());
canvas.style.cursor = 'grab';
// Buttons
document.getElementById('prevBtn').addEventListener('click', prevPage);
document.getElementById('nextBtn').addEventListener('click', nextPage);
// Set total pages span
const totalSpan = document.getElementById('totalPages');
if(totalSpan) totalSpan.innerText = TOTAL_PAGES;
// Initial draw with first page
renderCurrentPage();
attachDragEvents();
// optional: resize handling - keep crisp ratio
window.addEventListener('resize', () =>
renderCurrentPage();
);
)();
</script>
</body>
</html>
Creating a flipbook animation on is a popular project for web developers looking to master 3D CSS transforms and JavaScript interactions. These digital flipbooks simulate the tactile feel of physical books with smooth page-turning effects. 1. The Core Technology Most modern flipbooks on CodePen rely on CSS 3D Transforms transform-style: preserve-3d;
: This property is applied to the book container to ensure that child elements (the pages) exist in a 3D space rather than being flattened against the screen. perspective
: Set on a parent element, this defines how "far" the user is from the 3D object, making the flip appear realistic rather than distorted. backface-visibility: hidden;
: Essential for preventing the "back" of a page from showing through the front while it is flipping. 2. Common Implementation Methods Developers typically use one of two approaches: : Uses the
state of hidden checkboxes or radio buttons to trigger animations. When a user "clicks" (toggles the checkbox), the page's transform: rotateY() value changes from 0 raised to the composed with power negative 180 raised to the composed with power JavaScript/GSAP flipbook codepen
: For more complex interactions—like dragging a page corner or having a variable number of pages—libraries like GSAP (GreenSock) or plugins like are used to handle the math and physics of the turn. 3. Structural Breakdown A standard flipbook pen is usually structured as follows: Book Container
: The relative-positioned wrapper that holds the perspective.
: Absolute-positioned divs stacked on top of each other. Each page is divided into a "front" and "back" face. Stacking Order (
: This is the trickiest part. Developers must dynamically update the Creating a flipbook animation on is a popular
as pages flip so that the currently moving page stays on top of the others. 4. Top Examples on CodePen
Searching for "flipbook" on CodePen reveals several high-quality templates: Interactive Book : Features realistic page shadows and a "hardcover" effect. Magazine Layout
: Focuses on high-quality imagery and typography that shifts slightly as the page bends. Photo Album
: Uses simple left/right click navigation with a clean, minimalist 3D flip. 5. Learning Resources If you want to build one yourself, check out these guides: CSS-Tricks: Using Pure CSS for Flipbook Style Animation for a deep dive into the logic. MDN Web Docs on 3D Transforms to understand the underlying properties. to start your own flipbook project? the shadow rendering
Conclusion: Start Your Flipbook Project Today
The journey from "I need a flipbook" to "Here is my interactive brochure" is remarkably short if you leverage Codepen. The community has already solved the hardest parts: the 3D vertex math, the shadow rendering, and the drag detection.
By searching for "flipbook codepen" , you are not starting from scratch. You are standing on the shoulders of creative giants. Find a pen that matches your aesthetic (minimalist, realistic, or 3D), fork it, drop in your images, and tweak the CSS duration.
Whether you are building a poetry zine, a product catalog, or a nostalgic photo album, the tactile satisfaction of a digital page turn turns passive viewing into active reading.
Ready to flip? Open a new tab, type flipbook codepen into your search bar, and start forking the future of digital publishing.
Issue: "The pages are overlapping and look like a mess."
Solution: You forgot backface-visibility: hidden;. Without this, when a page rotates 180 degrees, you see the back of the front page instead of the front of the back page. Add this to your .page, .front, .back rules.
2. Scroll-based Flip
Bind the frame change to wheel events or page scroll percentage. Feels like flipping a real book.
canvas.addEventListener('wheel', (e) =>
if (e.deltaY > 0)
currentFrame = Math.min(totalFrames-1, currentFrame+1);
else
currentFrame = Math.max(0, currentFrame-1);
drawFrame(currentFrame);
);