Page Flip Effect — JavaScript Generator & Demo

Create a realistic page flip effect with JavaScript & WebGL. Adjust speed, direction & trigger, then copy the full source code. No jQuery required.

Page Flip Effect Generator

Adjust the speed, flip direction and trigger mode. The preview updates live. Click or hover the canvas to see the effect. When you're happy, copy the full standalone HTML.

Loading images…

Click to flip

FastSlow

Your code is ready

Copy the generated HTML & JS to your project

How the Page Flip Effect Works

1. Two images as GPU textures

Both images are loaded and uploaded to the GPU as WebGL textures. The fragment shader receives them as uTex0 (outgoing page) and uTex1 (incoming page). The shader classifies each pixel into one of four regions: behind the curl, flat outgoing page, on the cylinder surface, or the back face, and renders each differently.

2. Diagonal cylinder with rotation matrices

The page curl axis is tilted at 100° using a rotation matrix (pcRot). Every pixel's UV coordinates are transformed into this rotated space, and its distance from the cylinder axis (pcYC) determines which region it falls in. A second inverse matrix (pcRRot) maps cylinder surface points back to texture space for sampling.

3. Three rendering regions

When pcYC < -pcCylR, the pixel is behind the curl and shows the incoming page with a soft drop shadow cast by the cylinder. When pcYC > pcCylR, it's on the flat outgoing page. Otherwise it's on the cylinder itself. The shader computes the arc angle, decides whether the pixel is on the front face, the back face, or an edge-shadow region, and samples accordingly.

4. Back-of-page and shading

Pixels on the back face of the curling page are rendered as a bright gray surface (pcGray) that varies with the cylinder's curvature, lighter at the crest, slightly darker at the edges. Anti-aliased edge blending (pcAA) smooths the transition between the cylinder surface and the background behind it.

5. Animation loop

On trigger, the target progress toggles between 0 and 1. Each frame, the current value lerps toward the target using cur += (tgt - cur) * speed, creating a smooth ease-out. The animation loop only runs while the values differ, with no wasted frames. The speed value is what you control with the slider above.

Page Flip Effect: Fragment Shader

Here's the key fragment shader that creates the page curl. It takes two textures, a progress uniform, and produces the entire effect in a single pass.

// InvertedPageCurl — HP / Sergey Kosarevsky (BSD-3 Clause)
precision mediump float;
uniform sampler2D uTex0;    // outgoing page
uniform sampler2D uTex1;    // incoming page
uniform float     uProgress; // 0 → 1
uniform float     uDir;      // 1.0 = right-to-left, -1.0 = left-to-right
varying vec2 vUV;

const float pcPI    = 3.141592653589793;
const float pcCylR  = 1.0 / pcPI / 2.0; // cylinder radius
const float pcScale = 512.0;
const float pcSharp = 3.0;

// Map a point on the cylinder back to texture space
vec3 pcHitPt(float ha, float yc, vec3 pt, mat3 rr) {
  pt.y = ha / (2.0 * pcPI);
  return rr * pt;
}

// Anti-aliased edge blend
vec4 pcAA(vec4 c1, vec4 c2, float d) {
  d *= pcScale;
  if (d < 0.0) return c2;
  if (d > 2.0) return c1;
  float dd = pow(1.0 - d / 2.0, pcSharp);
  return (c2 - c1) * dd + c1;
}

// Distance from pt to nearest edge of [0,1]²
float pcEdgeDist(vec3 pt) {
  float dx = abs(pt.x > 0.5 ? 1.0 - pt.x : pt.x);
  float dy = abs(pt.y > 0.5 ? 1.0 - pt.y : pt.y);
  if (pt.x < 0.0) dx = -pt.x;
  if (pt.x > 1.0) dx = pt.x - 1.0;
  if (pt.y < 0.0) dy = -pt.y;
  if (pt.y > 1.0) dy = pt.y - 1.0;
  if ((pt.x < 0.0 || pt.x > 1.0) && (pt.y < 0.0 || pt.y > 1.0))
    return sqrt(dx * dx + dy * dy);
  return min(dx, dy);
}

void main() {
  float t   = clamp(uProgress, 0.0, 1.0);
  float dir = uDir;
  vec2  pcP = vUV;  // outgoing texture coords
  vec2  pcQ = vUV;  // incoming texture coords

  // Map progress to cylinder sweep range [-0.16 … 1.5]
  float pcAmt    = t * 1.66 - 0.16;
  float pcCylAng = 2.0 * pcPI * pcAmt;

  // 100° rotation matrix — gives the diagonal page-curl angle
  float pcDeg  = 100.0 * pcPI / 180.0;
  float pcCosA = cos(pcDeg);
  float pcSinA = sin(pcDeg);

  mat3 pcRot  = mat3( pcCosA, -pcSinA, 0.0,
                      pcSinA,  pcCosA, 0.0,
                     -0.801,   0.890,  1.0);
  mat3 pcRRot = mat3( pcCosA,  pcSinA, 0.0,
                     -pcSinA,  pcCosA, 0.0,
                      0.985,   0.985,  1.0);

  vec2  pcGeomP = dir > 0.0 ? pcP : vec2(1.0 - pcP.x, pcP.y);
  vec3  pcPt    = pcRot * vec3(pcGeomP, 1.0);
  float pcYC    = pcPt.y - pcAmt;

  float pcShadowCarry = 0.0;
  float tr = 0.0;
  vec2  uv = pcP;

  if (pcYC < -pcCylR) {
    // Behind the curl — show incoming page with cylinder shadow
    float pcSh  = 0.0;
    float pcYN  = -pcCylR - pcCylR - pcYC;
    float pcHA2 = (acos(clamp(pcYN / pcCylR, -1.0, 1.0)) + pcCylAng) - pcPI;
    vec3  pcBP  = pcHitPt(pcHA2, pcYN, pcPt, pcRRot);
    if (pcYN < 0.0 && pcBP.x >= 0.0 && pcBP.y >= 0.0 &&
        pcBP.x <= 1.0 && pcBP.y <= 1.0 && (pcHA2 < pcPI || pcAmt > 0.5)) {
      pcSh = (1.0 - sqrt(pow(pcBP.x - 0.5, 2.0) + pow(pcBP.y - 0.5, 2.0)) / 0.71)
             * pow(-pcYN / pcCylR, 3.0) * 0.5;
    }
    gl_FragColor = vec4(texture2D(uTex1, pcQ).rgb - pcSh, 1.0);
    return;

  } else if (pcYC > pcCylR) {
    // Flat outgoing page — no distortion
    tr = 0.0; uv = pcP;

  } else {
    float pcHA    = (acos(clamp(pcYC / pcCylR, -1.0, 1.0)) + pcCylAng) - pcPI;
    float pcHAMod = mod(pcHA, 2.0 * pcPI);

    if ((pcHAMod > pcPI && pcAmt < 0.5) || (pcHAMod > pcPI / 2.0 && pcAmt < 0.0)) {
      // See-through the cylinder
      float pcStHA = pcPI - (acos(clamp(pcYC / pcCylR, -1.0, 1.0)) - pcCylAng);
      vec3  pcStPt = pcHitPt(pcStHA, pcYC, pcPt, pcRRot);
      if (pcYC <= 0.0 && (pcStPt.x < 0.0 || pcStPt.y < 0.0 ||
                           pcStPt.x > 1.0 || pcStPt.y > 1.0)) {
        gl_FragColor = texture2D(uTex1, pcQ); return;
      } else if (pcYC > 0.0) {
        tr = 0.0; uv = pcP;
      } else {
        vec2 pcStUV = dir > 0.0 ? pcStPt.xy : vec2(1.0 - pcStPt.x, pcStPt.y);
        tr = 0.0; uv = pcStUV;
      }

    } else {
      vec3 pcCylPt = pcHitPt(pcHA, pcYC, pcPt, pcRRot);
      if (pcCylPt.x < 0.0 || pcCylPt.y < 0.0 ||
          pcCylPt.x > 1.0 || pcCylPt.y > 1.0) {
        // Cylinder edge casts shadow
        float pcStHA2  = pcPI - (acos(clamp(pcYC / pcCylR, -1.0, 1.0)) - pcCylAng);
        vec3  pcStPt2  = pcHitPt(pcStHA2, pcYC, pcPt, pcRRot);
        float pcShadow = max(0.0, (1.0 - pcEdgeDist(pcCylPt) * 30.0) / 3.0) * pcAmt;
        if (pcYC <= 0.0 && (pcStPt2.x < 0.0 || pcStPt2.y < 0.0 ||
                             pcStPt2.x > 1.0 || pcStPt2.y > 1.0)) {
          vec4 pcThr = texture2D(uTex1, pcQ);
          pcThr.rgb -= pcShadow;
          gl_FragColor = pcThr; return;
        } else if (pcYC > 0.0) {
          tr = 0.0; uv = pcP; pcShadowCarry = pcShadow;
        } else {
          vec2 pcSt2UV = dir > 0.0 ? pcStPt2.xy : vec2(1.0 - pcStPt2.x, pcStPt2.y);
          tr = 0.0; uv = clamp(pcSt2UV, 0.0, 1.0); pcShadowCarry = pcShadow;
        }

      } else {
        // Backside of the curled page (bright gray surface)
        float pcGray = 0.8 * (pow(1.0 - abs(pcYC / pcCylR), 0.2) / 2.0 + 0.5);
        vec4  pcBack = vec4(vec3(pcGray), 1.0);
        vec4  pcOther;
        if (pcYC < 0.0) {
          float pcSh2 = (1.0 - sqrt(pow(pcCylPt.x - 0.5, 2.0) +
                                     pow(pcCylPt.y - 0.5, 2.0)) / 0.71)
                        * pow(-pcYC / pcCylR, 3.0) * 0.5;
          pcOther = vec4(0.0, 0.0, 0.0, pcSh2);
        } else {
          pcOther = texture2D(uTex1, pcQ);
        }
        vec4  pcBackFinal = pcAA(pcBack, pcOther, pcCylR - abs(pcYC));
        float pcDist      = pcEdgeDist(pcCylPt);
        float pcShadow3   = max(0.0, (1.0 - pcDist * 30.0) / 3.0) * pcAmt;
        vec4  pcThruClr   = texture2D(uTex1, pcQ);
        pcThruClr.rgb    -= pcShadow3;
        gl_FragColor = pcAA(pcBackFinal, pcThruClr, pcDist);
        return;
      }
    }
  }

  // Flat outgoing page with optional shadow carry
  vec4 city   = texture2D(uTex0, uv);
  city.rgb   -= pcShadowCarry;
  gl_FragColor = mix(city, texture2D(uTex1, pcQ), tr);
}

The JavaScript side is simple: load two images as textures, compile the shader, and animate uProgress on click or hover. The speed slider above controls the lerp factor used each frame.

// Lerp-based animation loop
var cur = 0, tgt = 0, raf = null;
var SPEED = 0.055; // increase = faster, decrease = slower

function draw() {
  cur += (tgt - cur) * SPEED;
  if (Math.abs(tgt - cur) < 0.001) {
    cur = tgt; raf = null;
  } else {
    raf = requestAnimationFrame(draw);
  }
  gl.uniform1f(uProg, cur);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}

// Click trigger
wrap.addEventListener('click', function() {
  tgt = tgt < 0.5 ? 1 : 0;
  if (!raf) raf = requestAnimationFrame(draw);
});

// Hover trigger (alternative)
// wrap.addEventListener('mouseenter', function() { tgt = 1; if (!raf) raf = requestAnimationFrame(draw); });
// wrap.addEventListener('mouseleave', function() { tgt = 0; if (!raf) raf = requestAnimationFrame(draw); });

How the Book Flip Works

The Book Flip stacks multiple leaves on top of each other. Each leaf has a front face and a back face. Adding a class triggers a CSS rotateY(-180deg) rotation, peeling the top leaf away to reveal the one underneath.

1. The leaf structure

Each leaf is position: absolute; inset: 0 so all leaves occupy the same space. The front and back faces sit inside with backface-visibility: hidden so only the correct side is visible at any time:

<div class="book-scene">
  <div style="width:480px; display:flex; justify-content:flex-end;">
    <div class="book">
      <div class="leaf" id="leaf-0">
        <div class="front">Page 1</div>
        <div class="back">Page 2</div>
      </div>
      <div class="leaf" id="leaf-1">
        <div class="front">Page 3</div>
        <div class="back">Page 4</div>
      </div>
    </div>
  </div>
</div>

2. The core CSS

Five properties drive the entire effect. perspective on the scene adds the 3D depth. transform-origin: left center pins the rotation to the left edge, the spine. rotateY(-180deg) flips the leaf over. backface-visibility: hidden hides the rear of each face while it's pointing away:

.book-scene {
  perspective: 2000px; /* 3D depth — lower = more dramatic */
}

.leaf {
  position: absolute;
  inset: 0;
  transform-origin: left center; /* rotation axis = the spine */
  transform-style: preserve-3d;
  transition: transform 0.8s cubic-bezier(0.645, 0.045, 0.355, 1.000);
}

.leaf.flipped {
  transform: rotateY(-180deg); /* flip! */
}

.front, .back {
  position: absolute;
  inset: 0;
  backface-visibility: hidden; /* hide the face pointing away */
}

.back {
  transform: rotateY(180deg); /* pre-rotate the back face */
}

3. Z-index management

Leaves start stacked with the first leaf on top (z-index: 4, 3, 2, 1). During a flip, the animating leaf is raised to z-index: 99 so it sweeps above everything. After the flip completes, it's lowered below the remaining unflipped leaves.

How the CSS Page Flip Works

The CSS Page Flip renders an open book with a visible spine. Each leaf occupies only the right half of the book container. When flipped, the leaf rotates around the spine and its back face becomes the left page of the spread, creating an authentic two-page reading experience.

1. The two-page spread layout

All leaves are anchored to the right with right: 0; width: 50%. The rotation axis is transform-origin: left center, positioned exactly at the spine. The parent gets perspective for depth:

.book-scene {
  perspective: 2500px; /* adjustable — controls 3D depth */
}

.book {
  position: relative;
  width: 500px;
  height: 310px;
}

.leaf {
  position: absolute;
  right: 0;          /* anchored to right half */
  width: 50%;        /* each leaf = one page width */
  height: 100%;
  transform-origin: left center; /* = the spine */
  transform-style: preserve-3d;
  transition: transform 1s ease;
}

.leaf.flipped {
  transform: rotateY(-180deg); /* sweeps from right to left */
}

2. Front and back faces

The front face is the right-hand page before the flip. The back face, pre-rotated 180°, becomes the left-hand page after the flip. Both use backface-visibility: hidden:

.front, .back {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
}

.back {
  transform: rotateY(180deg);
  /* After the leaf flips -180deg, this face is now
     pointing toward the viewer on the LEFT side */
}

3. What the perspective slider does

The perspective value controls how extreme the 3D distortion looks during the flip. A low value (e.g. 600px) creates a very foreshortened, dramatic curl. A high value (e.g. 5000px) produces a nearly flat, subtle rotation, closer to a simple slide.

Page Flip Effect as a Scroll Transition

The generator above flips between two images on click or hover. With fullPage.js, you get the same page flip effect between fullscreen sections on scroll, with touch support, snap scrolling, and keyboard navigation built in.

Choose a preset, adjust the speed, and scroll inside the preview to see the transition live:

Loading preview...

fullPage.js adds

  • 80+ WebGL & CSS transitions
  • Touch & swipe support
  • Scroll snapping
  • Keyboard navigation
  • Anchor links & history
  • Lazy loading
  • 60fps GPU performance
Get fullPage.js →

Scroll inside the preview to see the page flip transition between sections

When to Use a Page Flip Effect

Digital magazines

Page flip effects simulate physical page turns, making online publications feel tactile and familiar to readers.

Story-driven landing pages

Fullscreen sections that flip like book pages create a narrative flow, and each scroll reveals the next chapter.

Portfolio presentations

The page curl adds drama to project showcases. Each section transition feels like turning to the next piece.

Image galleries

Photo stories and lookbooks where the page flip connects images with a sense of physical continuity.

FAQ

How do I create a page flip effect with JavaScript?
Use a WebGL canvas with the InvertedPageCurl fragment shader. Load two images as GPU textures, then animate a uProgress uniform from 0 to 1. The shader transforms pixel UVs into a rotated cylinder space, classifies each pixel as behind the curl, on the flat page, on the cylinder front, or on the cylinder back, and renders each region with proper shading and drop shadows. Use the interactive generator above to configure speed and direction, then copy the full code.
Can I make a page flip effect without jQuery?
Yes, jQuery is not needed. Modern page flip effects use vanilla JavaScript with the WebGL API (supported in all modern browsers) or CSS 3D transforms (for simpler flat rotations). The demo on this page uses zero external libraries.
How do I add a page flip transition between scroll sections?
Use fullPage.js with the Cinematic extension. Set cinematic: true and cinematicOptions: { effect: 'page' } to get a realistic WebGL page flip between fullscreen sections on scroll. For a simpler CSS-based 3D rotation, use the Effects extension with effects: 'cube'.
What is the difference between a page flip and a card flip effect?
A page flip simulates turning a physical page: the surface curls like paper with 3D depth and shadows. It requires WebGL or canvas. A card flip rotates a flat element on its Y or X axis using CSS 3D transforms (perspective, rotateY, backface-visibility). Card flips are simpler but less realistic.

Make Your Website Stand Out with Fullscreen Effects

Add scroll transitions, parallax and animations to your site with fullPage.js. One component, 80+ effects.

Brandire
New World of Work
OW Consulting

Powering fullscreen design for Google, Sony, BBC, eBay & more

Get fullPage.js
+ Suggest Effect