你可能在机场、车站大屏、或者是手机上的翻页时钟小组件里见过这种效果——黑色的卡牌,数字像日历一样“啪嗒”一声翻过去,带着一点机械的阻尼感。这个效果用 HTML+CSS+JS 做起来,其实没有想象中复杂,核心也就几十行代码。它不依赖任何框架和库,是纯前端三件套的经典组合。
这个项目的价值在于:它同时练到了 CSS 3D 变换、DOM 状态同步、JS 定时器驱动这三块前端基本功。对刚学完 HTML/CSS/JS 基础、想找个练手项目的人来说,翻页时钟是一个非常好的过渡——从“会写静态页面”到“能写出带交互和动效的小应用”。这篇文章我会把原理、踩过的坑、完整代码一次性讲清楚,你跟着走一遍就能跑起来。
1. 整体设计思路:先想清楚翻页时钟是怎么“演”出来的
1.1 翻页效果的本质:一次“视觉欺骗”
先说结论:翻页时钟不是靠 JS 一帧一帧算动画算出来的,而是用 CSS 的 3D 变换做视觉欺骗。JS 只干一件事——到点了,告诉数字“该换了”。
真实的翻页钟翻动过程,你仔细观察的话,其实包含两个阶段:
- 旧数字的上半部分先像书页一样向下翻倒,露出下面新数字的上半部分。
- 紧接着,新数字的下半部分从水平位置向上立起,压在旧数字下半部分的上面。
这两个动作连起来,人眼看到的就是“整张卡片”翻了过去。但如果你拆开看,每一时刻屏幕上其实都是多个半层在配合演出。前端要复现这个效果,最直接的办法就是把这个过程拆成 CSS 动画,让旧上半张翻转,再让新下半张立起。
理解了这个原理,后面所有代码逻辑都好办了:我们不是在做一个“数字变化”,而是在做“两个半张纸片的翻动”。
1.2 为什么是 HTML+CSS+JS 三件套,而不是其他方案
有人可能问:纯 CSS 能不能做?纯 CSS 能做出翻转动画,但它拿不到当前时间,也无法决定“什么时候该翻、翻到几”。翻页时钟的核心驱动是时间数据,CSS 再厉害也只是展示层,数据层必须靠 JS。
也有人问:用 Canvas 画不是更炫吗?Canvas 当然能做,但为了一个简单的时钟去写 Canvas 绘制逻辑,代码量会明显增加,而且 DOM 的可访问性和可维护性都比 Canvas 好。翻页时钟的关键恰恰在于“用最简单的结构表达清晰的逻辑”,HTML 负责骨架、CSS 负责表情、JS 负责大脑,各司其职。这个分工本身就是这门语言最初的哲学,也是一个值得新手好好体会的工程思维。
1.3 组件结构:一位数字由四个层组成
每个数字位,在 HTML 结构上是一个独立的卡片组件。卡片内部我拆成四个层:
| 层名 | 位置 | 角色 | 显示内容 |
|---|---|---|---|
.face-top |
卡片上半区 | 静态层 | 当前数字的上半部分 |
.face-bottom |
卡片下半区 | 静态层 | 当前数字的下半部分 |
.flip-top |
卡片上半区 | 动画层 | 旧数字的上半部分,负责翻走 |
.flip-bottom |
卡片下半区 | 动画层 | 新数字的下半部分,负责立起 |
一个完整的时钟由六张卡片组成,分别代表 时十位、时个位、分十位、分个位、秒十位、秒个位,中间用冒号分隔。
拿到这个结构之后,再回头看翻页过程就清晰多了:
- 平时你看到的数字,是
face-top和face-bottom拼出来的。 - 动画开始时,
flip-top带着旧数字上半部分翻倒,露出下面已经换成新数字的face-top。 - 紧接着
flip-bottom带着新数字下半部分立起,盖住还没更新的face-bottom。 - 动画结束,把
face-bottom也换成新数字,整个卡片就完成了“翻页”。
这个设计里有一个特别重要的点:face-top 的内容要在动画开始前就换成新值,因为它此时被 flip-top 挡着,用户看不见。等到 flip-top 翻走,它露出来的瞬间必须是新数字,否则就穿帮了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心原理:CSS 3D 变换与翻页的关键细节
2.1 transform、transform-origin、backface-visibility 三板斧
翻页效果的核心是 CSS 里的 transform: rotateX(角度)。rotateX 是绕水平轴旋转,元素会像百叶窗的叶片一样“向前倒”或“向后仰”。如果你用的是 rotateY,那就是绕垂直轴旋转,效果像推拉门,方向完全不对。
光会旋转还不够,还得指定旋转的轴心,也就是 transform-origin。这个属性决定元素围绕哪个点转:
transform-origin: bottom center:元素以底边为轴旋转,适合“上半张向下翻”。transform-origin: top center:元素以顶边为轴旋转,适合“下半张向上立”。
这个轴心就是物理中的“铰链”,装错了位置,翻页效果就变成整个卡片转圈了。
还有一个新手特别容易忽略的属性:backface-visibility: hidden。当元素旋转超过 90 度后,你会从背面看到它的镜像内容,就像从纸的背面看字一样,是反的。翻页时钟的翻页过程,元素会转到接近 90 度,如果不把背面隐藏,屏幕上就会出现一片镜像残影,非常出戏。加一行 backface-visibility: hidden 就能解决。
2.2 用 overflow: hidden 裁出“半张数字”
数字在卡片里是完整显示的,但翻页时钟需要“只露出上半部分”或“只露出下半部分”。这里我不用 clip-path,因为它对 3D 变换的支持在部分浏览器里有兼容性问题。我用的办法是:外层容器高度固定为半个卡片,加 overflow: hidden,内层放一个完整高度的数字文本,通过偏移只露出需要的那一半。
以卡片高 88px 为例:
- 数字文本放在一个
height: 88px、line-height: 88px的 span 里,这样文字天然垂直居中在整张卡片中间。 - 上半层容器高度是 44px,
overflow: hidden,span 顶部对齐容器顶部,于是只能看到数字的上半部分。 - 下半层容器高度也是 44px,span 通过
margin-top: -44px向上偏移,于是只能看到数字的下半部分。
这样上下两个半层拼起来,视觉上就是一个完整居中的数字字。关键在于两个半层的偏移量要一致,稍微差 1px,数字拼接处就会错位。我建议上下两层用同一套变量来控制,比如卡片高度用 CSS 变量 --card-height,往下半层偏移的时候直接用 calc(var(--card-height) / -2),这样以后改卡片尺寸,数字对齐也不会乱。
2.3 动画时序:上半先倒,下半后立
翻页动画的时间轴设计是一个容易被人忽略但又极其影响质感的部分。
我用的时序是:
flip-top:动画时长 0.3s,缓动函数ease-in,从rotateX(0)翻到rotateX(-90deg)。flip-bottom:动画时长 0.3s,缓动函数ease-out,延迟 0.3s 后从rotateX(90deg)立起到rotateX(0)。
为什么要 ease-in 和 ease-out,而不是默认的 ease?我是这样理解的:现实里卡纸失去支撑后会加速下落,接近自由落体的感觉,所以上半张用 ease-in,越翻越快;下半张立起来的时候,先是快速抬起,到接近竖直时速度放慢,带一点缓冲,所以用 ease-out。这个细节你在真机上对比一下就能感觉到差别——用对了,动画就有“重感”,用错了,整个翻页像塑料片在飘。
总动画时间是 0.6s。JS 里的状态复位要在 0.6s 之后做,我一般留 50ms 的余量,用 setTimeout(..., 650),避免动画还没结束就把层复位导致闪烁。
3. 完整实操:从零写一个 HTML+CSS+JS 翻页时钟
3.1 HTML 骨架搭建
我的目录结构很简单:
text复制flip-clock/
├── index.html
├── style.css
└── script.js
先看 HTML。为了让首次被打开的页面不出现“00:00:00”到正确时间之间的闪烁,我直接把静态层的默认值写成了 0,等 JS 初始化后再替换成真实时间。
html复制<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>翻页时钟</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="clock" id="clock">
<!-- 每一位数都是一个 clock-item,结构完全一致 -->
<div class="clock-item" data-unit="h1">
<div class="face face-top"><span>0</span></div>
<div class="face face-bottom"><span>0</span></div>
<div class="flip flip-top"><span>0</span></div>
<div class="flip flip-bottom"><span>0</span></div>
</div>
<div class="clock-item" data-unit="h2">
<div class="face face-top"><span>0</span></div>
<div class="face face-bottom"><span>0</span></div>
<div class="flip flip-top"><span>0</span></div>
<div class="flip flip-bottom"><span>0</span></div>
</div>
<div class="colon">:</div>
<div class="clock-item" data-unit="m1">
<div class
