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);