旗下品牌:
石家庄网站开发 石家庄网站开发公司

资讯动态

察而思、思而行、行而后语、知行合一

用 HTML + CSS + JS 实现杂志翻页效果

发布时间:2026-10-23 热度:

翻页阅读是数字杂志、电子书、产品画册最经典的交互形式。它既要有纸媒的仪式感,又要在浏览器里跑得流畅。本文从渲染原理出发,带你从零实现一个可用的 3D 翻页组件,并讨论几个工程上绕不开的坑。


一、技术选型:三种实现路线

在动手之前,先明确有哪些可选方案:

方案原理优点缺点
CSS 3D TransformrotateY + perspective实现简单、可访问性好、文字可选中只能做刚性平面翻页,做不出纸张弯曲
Canvas 2D逐帧绘制页面变形可以做软翻页(curl)、任意形变文字不可选中、清晰度受 DPR 影响
WebGL纹理网格顶点位移效果最接近真实纸张成本最高,移动端兼容与性能需打磨

绝大多数杂志站用的是第一条路线:DOM + CSS 3D。原因很现实——杂志内容含大量文字、图片、甚至嵌入视频和表单,只有 DOM 方案能让这些内容保持可用。本文也以这条路线为主线。


二、核心原理:三个关键概念

1. 透视(perspective)

perspective 定义了"观察者到 z=0 平面的距离"。值越小,近大远小越夸张:

css
.book {
  perspective: 2400px;        /* 约等于真实翻书的观感 */
  perspective-origin: 50% 50%; /* 观察点在书脊中央 */}

小于 1000px 会明显"镜头贴脸",大于 4000px 又接近正交投影、失去立体感。2000~3000px 是最舒服的区间。

2. 旋转轴(transform-origin)

一张纸绕哪里转?答案是书脊。所以在布局上,把每张纸定位到书脊右侧,然后把旋转原点设在它的左边缘:

css
.leaf {
  position: absolute;
  left: 50%;                      /* 左边缘正好落在书脊上 */
  width: 50%;
  height: 100%;
  transform-origin: left center;  /* 绕书脊旋转 */}

这样 rotateY(-180deg) 就等价于"把这一页从右翻到左"。

3. 背面隐藏(backface-visibility)

这是正反面分离的关键。每张纸由前后两个面组成,背面元素预先旋转 180°:

css
.leaf__face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;}.leaf__face--back {
  transform: rotateY(180deg);}

逻辑是这样的:纸旋转 0° 时,正面朝向你;旋转 -180° 时,正面背向你被隐藏,而背面元素因预先转了 180°,此时正好正对你 —— 于是你就看到了"这一页的反面"。不需要 JS 去切换内容,纯 CSS 自动完成。


三、完整实现

1. HTML 结构

只需要一个容器,页面由 JS 动态生成:

html
<div class="book" id="book"></div><nav class="controls">
  <button id="btnPrev" aria-label="上一页">‹</button>
  <span id="indicator">1 / 6</span>
  <button id="btnNext" aria-label="下一页">›</button></nav>

2. CSS

css
* { box-sizing: border-box; }body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  background: radial-gradient(circle at 50% 40%, #2b2f38, #14161a 70%);
  font-family: "Georgia", "Songti SC", serif;
  color: #222;}/* ---------- 书本容器:提供透视 ---------- */.book {
  position: relative;
  width: min(92vw, 900px);
  aspect-ratio: 3 / 2;          /* 每页比例 3:4,接近杂志开本 */
  perspective: 2400px;
  perspective-origin: 50% 50%;
  user-select: none;
  touch-action: pan-y;          /* 允许纵向滚动,横向交给手势 */}/* 书脊阴影(纯装饰) */.book::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  width: 44px;
  height: 100%;
  transform: translateX(-50%);
  background: linear-gradient(to right,
    transparent,
    rgba(0,0,0,.16) 45%,
    rgba(0,0,0,.16) 55%,
    transparent);
  pointer-events: none;
  z-index: 9999;}/* ---------- 一张纸 ---------- */.leaf {
  position: absolute;
  top: 0;
  left: 50%;
  width: 50%;
  height: 100%;
  transform-origin: left center;
  transform-style: preserve-3d;
  transition: transform 800ms cubic-bezier(.42, .05, .25, 1);
  will-change: transform;}.leaf.is-flipped {
  transform: rotateY(-180deg);}/* ---------- 纸张的正反面 ---------- */.leaf__face {
  position: absolute;
  inset: 0;
  overflow: hidden;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  background: #fdfcf8;
  box-shadow: 0 8px 28px rgba(0, 0, 0, .35);}.leaf__face--front {
  border-radius: 0 4px 4px 0;
  background-image: linear-gradient(to left, rgba(0,0,0,.10), transparent 18%);}.leaf__face--back {
  border-radius: 4px 0 0 4px;
  transform: rotateY(180deg);
  background-image: linear-gradient(to right, rgba(0,0,0,.10), transparent 18%);}/* ---------- 页面内容排版 ---------- */.page {
  position: absolute;
  inset: 0;
  padding: 9% 11%;
  display: flex;
  flex-direction: column;
  gap: 12px;}.page__index {
  font: 600 13px/1 ui-monospace, monospace;
  letter-spacing: .18em;
  color: #b08d57;}.page__title {
  margin: 0;
  font-size: clamp(16px, 2.2vw, 26px);
  line-height: 1.3;}.page__body {
  margin: 0;
  font-size: clamp(11px, 1.15vw, 14px);
  line-height: 1.9;
  color: #4a4a4a;
  column-count: 2;              /* 报纸式多栏 */
  column-gap: 22px;
  text-align: justify;}/* ---------- 降级:尊重「减少动效」偏好 ---------- */@media (prefers-reduced-motion: reduce) {
  .leaf { transition-duration: 1ms; }}

3. JavaScript

js
const book      = document.getElementById('book');const indicator = document.getElementById('indicator');const TOTAL_PAGES = 12;        // 总页数,必须是偶数const FLIP_MS     = 800;       // 与 CSS transition 时长保持一致const leaves = [];             // 每张纸对应 DOM 元素let flippedCount = 0;          // 已翻过去的纸张数量/* ---------- 1. 生成 DOM ---------- */for (let i = 0; i < TOTAL_PAGES / 2; i++) {
  const leaf = document.createElement('div');
  leaf.className = 'leaf';

  // 第 i 张纸:正面 = 第 2i 页,背面 = 第 2i+1 页
  leaf.innerHTML = `
    <div class="leaf__face leaf__face--front">${pageHTML(i * 2)}</div>
    <div class="leaf__face leaf__face--back">${pageHTML(i * 2 + 1)}</div>
  `;

  book.appendChild(leaf);
  leaves.push(leaf);}function pageHTML(n) {
  return `
    <article class="page">
      <span class="page__index">${String(n + 1).padStart(2, '0')}</span>
      <h2 class="page__title">章节标题 ${n + 1}</h2>
      <p class="page__body">
        这里是杂志正文内容。杂志排版的精髓在于分栏、留白与图文的节奏感。
        通过 CSS 的 column-count 属性,可以低成本模拟出报刊式的两栏布局,
        再配合 serif 字体与宽松的行高,阅读体验会非常接近纸媒。
      </p>
    </article>  `;}/* ---------- 2. 层级管理(本方案最容易出错的地方) ---------- */function updateStack() {
  const n = leaves.length;
  leaves.forEach((leaf, i) => {
    if (leaf.classList.contains('is-flipping')) return;  // 动画中的不参与
    // 未翻的纸:索引越小越靠上(第 1 张盖住第 2 张)
    // 已翻的纸:索引越大越靠上(后翻的盖住先翻的)
    leaf.style.zIndex = i < flippedCount ? i + 1 : 100 + (n - i);
  });}/* ---------- 3. 翻页动作 ---------- */function next() {
  if (flippedCount >= leaves.length) return;
  const leaf = leaves[flippedCount];

  leaf.classList.add('is-flipping');
  leaf.style.zIndex = 999;          // 动画期间置顶,避免穿模
  flippedCount++;
  leaf.classList.add('is-flipped');

  settle(leaf);}function prev() {
  if (flippedCount <= 0) return;
  flippedCount--;
  const leaf = leaves[flippedCount];

  leaf.classList.add('is-flipping');
  leaf.style.zIndex = 999;
  leaf.classList.remove('is-flipped');

  settle(leaf);}/* 动画结束后收尾(transitionend 可能因中断不触发,加兜底定时器) */function settle(leaf) {
  let done = false;
  const finish = () => {
    if (done) return;
    done = true;
    leaf.classList.remove('is-flipping');
    updateStack();
    updateIndicator();
  };
  leaf.addEventListener('transitionend', finish, { once: true });
  setTimeout(finish, FLIP_MS + 80);}function updateIndicator() {
  indicator.textContent = `${Math.min(flippedCount + 1, leaves.length)} / ${leaves.length}`;}/* ---------- 4. 交互:点击 / 手势 / 键盘 ---------- */book.addEventListener('click', (e) => {
  const rect = book.getBoundingClientRect();
  (e.clientX - rect.left > rect.width / 2) ? next() : prev();});let startX = null;book.addEventListener('pointerdown', (e) => { startX = e.clientX; });book.addEventListener('pointerup', (e) => {
  if (startX === null) return;
  const dx = e.clientX - startX;
  if (dx < -40) next();
  else if (dx > 40) prev();
  startX = null;});document.addEventListener('keydown', (e) => {
  if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); next(); }
  if (e.key === 'ArrowLeft')  { e.preventDefault(); prev(); }});document.getElementById('btnNext').onclick = next;document.getElementById('btnPrev').onclick = prev;/* ---------- 5. 初始化 ---------- */updateStack();updateIndicator();

四、三个必须知道的坑

坑 1:z-index 不能交给浏览器

很多人以为开了 preserve-3d,浏览器就会按 3D 深度自动排序。实际上,一旦元素带有 opacity、filter、transform 等属性,就会创建新的堆叠上下文,排序规则会变得难以预测,表现为翻页过程中"纸从书里穿过去"。

结论:显式管理 z-index。 规则很简单:

  • 还没翻的纸 —— 索引越小越靠上

  • 已经翻过去的纸 —— 索引越大越靠上

  • 正在动画的那张 —— 临时给一个极大值

坑 2:动画中断导致状态卡死

用户快速连点"下一页"时,transitionend 可能不会触发(动画被新的动画打断)。如果不做兜底,is-flipping 类会永远留在元素上,导致 z-index 再也无法更新。

上面的 settle() 里用 setTimeout 做兜底 + done 标志位防重入,就是为了解决这个问题。

坑 3:移动端的合成层爆炸

每张纸都是独立的合成层。页数一多(比如 60 页的杂志),GPU 内存会急剧上升,低端安卓机直接掉帧。

优化建议:

  1. 只保留相邻 2~3 张纸在 DOM 里,其余按需创建 / 回收,或改用 content-visibility: auto。

  2. will-change: transform 不要常驻,翻页前加上、结束后移除。

  3. 移动端把 perspective 调大一点(比如 3000px),减少透视带来的重绘面积。

  4. 大图用 loading="lazy" 且设置 width / height,避免翻页时触发布局抖动。


五、想要更真实的纸感?

纯 rotateY 得到的是刚性平面翻页,纸不会有弯曲。想要真正的"纸感",需要更进一步的方案:

方案 A:分段变形(CSS)
把一页切成 N 条竖直薄片,每条独立旋转。成本高、接缝明显,实践中很少用。

方案 B:Canvas 软翻页
用贝塞尔曲线定义翻页的"卷曲"轮廓,再逐像素采样原图做映射。效果很好,但文字无法选中,且高分屏需要按 DPR 放大画布。

方案 C:WebGL 网格变形
把页面渲染成纹理贴到网格上,用顶点着色器做卷曲。效果天花板最高。

方案 D:直接用成熟库

库特点
StPageFlip现代、零依赖、Canvas 渲染,支持软翻页与硬翻页,移动端手感好
turn.js老牌方案,基于 jQuery,生态成熟但久未更新
PageFlip (react/page-flip)React 生态首选,API 友好
PDF.js + 上述任一库常见的 PDF 翻页阅读器组合

如果你的需求只是"杂志 H5 里能翻页、内容可交互",那本文这套 DOM 方案足够了;如果追求视觉表现力(比如品牌宣传页),再考虑 Canvas / WebGL。


六、小结

翻页效果的复杂度其实不在"翻"这个动作,而在于层级管理、动画状态机、性能边界这三件事上。核心代码不到 100 行,但每一条 z-index 规则、每一个 transitionend 兜底,都是踩过坑之后才补上去的。

建议的落地路径:

  1. 先用本文的 DOM 方案跑通主流程,验证内容排版

  2. 加上手势、键盘、无障碍属性(aria-label、焦点管理)

  3. 页数超过 20 页时,引入虚拟化,只保留相邻纸张

  4. 视觉要求高时,再迁移到 StPageFlip 或 WebGL 方案


下一篇:没有了

联系尚武科技
客户服务
石家庄APP开发
0311-83796180
为您提供售前购买咨询、解决方案推荐等1V1服务!
技术支持及售后
石家庄APP开发公司
0311-66682288
为您提供从产品到服务的全面技术支持 !
客户服务
石家庄小程序开发
石家庄小程序开发公司
加我企业微信
为您提供售前购买咨询、
解决方案推荐等1V1服务!
石家庄网站建设公司
咨询相关问题或预约面谈,可以通过以下方式与我们联系。
石家庄网站制作
在线联系:
石家庄Web开发
石家庄软件开发
石家庄软件开发公司
ADD/地址:
河北·石家庄
新华区西三庄大街86号河北互联网大厦B座二层
Copyright © 2008-2026尚武科技 保留所有权利。 冀ICP备12011207号-2 石家庄网站开发冀公网安备 13010502001294号《互联网平台公约协议》
Copyright © 2026 www.sw-tech.cn, Inc. All rights reserved