Flipbook Codepen [better] File
The Magic of Flipbooks: Top CodePen Examples and How to Build Your Own
Digital flipbooks are a fantastic way to add a tactile, interactive feel to your web projects. Whether you're building a portfolio, an e-magazine, or just a fun experiment, CodePen is the ultimate playground for discovering and creating these animations. Why Use a Flipbook? Unlike standard scrolling, a flipbook effect provides:
Tactile Engagement: It mimics the classic allure of a traditional catalog or book.
Focus: It allows readers to concentrate on one spread at a time without distracting ads. flipbook codepen
Professionalism: It can transform a plain PDF into a high-conversion interactive publication. Inspiration: Must-See CodePens
If you're looking for a starting point, check out these standout community creations:
<!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)
if (!ctx) return;
const w = canvas.width;
const h = canvas.height;
// Base background depending on page mood
const gradients = [
start: '#ffedd5', end: '#fed7aa' , // warm paper
start: '#e0f2fe', end: '#bae6fd' , // sky
start: '#f3e8ff', end: '#e9d5ff' , // lavender
start: '#dcfce7', end: '#bbf7d0' , // mint
start: '#fff1f0', end: '#fee2e2' , // blush
start: '#fef9c3', end: '#fde047' , // lemon
start: '#e0e7ff', end: '#c7d2fe' , // indigo soft
start: '#ffedd5', end: '#fed7aa' ,
start: '#ccfbf1', end: '#99f6e4' , // teal dream
start: '#ffe4e6', end: '#fecdd3' , // rose
start: '#e9f5ff', end: '#cffafe' , // arctic
start: '#f5f0e6', end: '#ede2cf' // vintage
];
const theme = gradients[(pageNumber-1) % gradients.length];
const grad = ctx.createLinearGradient(0, 0, w*0.2, h);
grad.addColorStop(0, theme.start);
grad.addColorStop(1, theme.end);
ctx.fillStyle = grad;
ctx.fillRect(0, 0, w, h);
// Decorative subtle grid or paper texture
ctx.save();
ctx.globalAlpha = 0.1;
ctx.strokeStyle = '#aa8e66';
ctx.lineWidth = 1;
for(let i = 0; i < w; i += 30)
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i, h);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, i % h);
ctx.lineTo(w, i % h);
ctx.stroke();
ctx.restore();
// Page border / shadow effect
ctx.save();
ctx.shadowBlur = 0;
ctx.strokeStyle = '#d4b48c';
ctx.lineWidth = 3;
ctx.strokeRect(8, 8, w-16, h-16);
ctx.restore();
// ---- PAGE-SPECIFIC ILLUSTRATIONS (flipbook story) ----
ctx.font = `bold $Math.floor(w * 0.07)px "Segoe UI", "Quicksand", system-ui`;
ctx.fillStyle = '#2c2b28';
ctx.shadowBlur = 0;
// Title / header per page
ctx.font = `600 $Math.floor(w * 0.055)px "Segoe UI"`;
ctx.fillStyle = '#3b2c1f';
ctx.fillText(`✨ Page $pageNumber ✨`, w * 0.1, h * 0.12);
ctx.font = `$Math.floor(w * 0.035)px "Segoe UI"`;
ctx.fillStyle = '#5e4b34';
// Dynamic story elements based on page number (creative flipbook narrative)
const stories = [
title: "The Dream Begins", emoji: "🌙", desc: "A tiny cat naps under a starry sky.", draw: drawMoonCat ,
title: "Flying Kite", emoji: "🪁", desc: "Cat chases a diamond kite through clouds.", draw: drawKite ,
title: "Star Hopping", emoji: "⭐", desc: "Leaping between glowing stars!", draw: drawStars ,
title: "Rainbow Trail", emoji: "🌈", desc: "Sliding down a rainbow bridge.", draw: drawRainbow ,
title: "Ocean of Clouds", emoji: "☁️", desc: "Swimming in cotton-candy clouds.", draw: drawClouds ,
title: "Galaxy Whiskers", emoji: "🌀", desc: "Whiskers touch distant galaxies.", draw: drawGalaxy ,
title: "Cosmic Tea Party", emoji: "🍵", desc: "Sipping stardust tea with friends.", draw: drawTeaParty ,
title: "Moon Crater", emoji: "🌕", desc: "Exploring moon's silvery surface.", draw: drawMoonCrater ,
title: "Constellation Cat", emoji: "🐾", desc: "Cat becomes a constellation.", draw: drawConstellation ,
title: "Shooting Star Wish", emoji: "🌠", desc: "Making a wish on a comet.", draw: drawShootingStar ,
title: "Nebula Garden", emoji: "🌸", desc: "Flowers that glow like nebulae.", draw: drawNebula ,
title: "Home Again", emoji: "🏠", desc: "Return to cozy Earth with new dreams.", draw: drawHomecoming
];
const story = stories[pageNumber-1]
// ----- 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>
The CSS Magic (Perspective & Preserve-3D)
The "flip" happens via transform-style: preserve-3d. The container gets a perspective: 2000px; to create depth. As the user clicks, the page rotates along the Y-axis from 0deg to -180deg. The Magic of Flipbooks: Top CodePen Examples and
The CSS
The magic happens here. We use preserve-3d to keep the 3D context and rotateY to turn the pages.
/* Layout setup */
body
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: #f0f0f0;
font-family: sans-serif;
.scene
width: 300px;
height: 400px;
perspective: 1000px; /* Depth of field */
.book
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
.page
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
cursor: pointer;
/* The flip animation logic */
transform-style: preserve-3d;
transition: transform 0.8s cubic-bezier(0.645, 0.045, 0.355, 1);
transform-origin: left center; /* Hinge on the left */
/* Visuals */
box-shadow: 0 0 5px rgba(0,0,0,0.1);
/* Front and Back faces of a page */
.front, .back
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden; /* Hide the back side when facing away */
display: flex;
justify-content: center;
align-items: center;
font-size: 1.5rem;
background: white;
border: 1px solid #ddd;
box-sizing: border-box;
.back
background: #fff;
transform: rotateY(180deg); /* The back is flipped by default */
/* Active state - this class is toggled by JS */
.page.flipped
transform: rotateY(-180deg);
/* Z-indexing so pages stack correctly */
/* We stack them in reverse order in HTML, or use z-index */
.page:nth-child(1) z-index: 4;
.page:nth-child(2) z-index: 3;
.page:nth-child(3) z-index: 2;
.page:nth-child(4) z-index: 1;
/* Helper styling */
.click-hint
position: absolute;
bottom: 20px;
font-size: 0.8rem;
color: gray;
Core techniques and trade-offs
Primary options:
- CSS 3D transforms (rotateY/rotateX) — simple, GPU-accelerated, great for many use cases; limited realism for complex curls.
- Canvas/WebGL — pixel-level control, realistic shading/warping possible; more complex and heavier to implement.
- SVG with CSS/JS — vector-based crispness, good for page shapes and masks.
- Libraries (Turn.js, StPageFlip, StPageFlip3D, Three.js) — speed up development but add dependencies and size.
Trade-offs:
- Simplicity vs realism: CSS transforms are easiest; Canvas/WebGL gives realism at complexity/perf cost.
- Accessibility: custom interactive widgets can be inaccessible if not designed properly.
- Mobile: touch interactions require careful event handling and testing.
- Performance: keep transforms on the compositor (transform, opacity) and avoid layout-triggering properties.
3. The "HTML5 Canvas Flipbook" (Frame by Frame)
Search tag: canvas flipbook codepen animation
This is a misnomer often used for actual animations drawn frame by frame on canvas. However, some advanced users combine canvas drawing with mouse events to create a "page pull" effect where the page bends based on cursor X/Y coordinates.
Best for: Interactive storytelling, game assets, custom vector folding.