让我们使用 CSS 自定义属性 和 calc()
函数 来构建一个功能齐全且可设置的“模拟”时钟。 然后,我们将其转换为“数字”时钟。 所有这些都不需要使用 JavaScript!
以下是我们即将制作的时钟的快速预览
calc()
函数的回顾
CSS 预处理器一直用计算数值 CSS 值的功能来取悦我们。 预处理器的问题在于,它们在 CSS 代码编译后的上下文中缺乏知识。 这就是为什么你无法说你想让你的元素宽度为容器的 100% 减去 50 像素的原因。 这会在预处理器中产生错误
width: 100% - 50px;
// error: Incompatible units: 'px' and '%'
正如它们的名称所暗示的那样,预处理器预处理你的指令,但它们的输出仍然是普通的 CSS,这就是为什么它们无法在你的算术运算中协调不同的单位的原因。 Ana 对 Sass 和 CSS 功能之间的冲突 进行了详细的介绍。
好消息是,原生 CSS 计算不仅是可能的,而且我们甚至可以使用 calc()
函数来组合不同的单位,例如像素和百分比
width: calc(100% - 50px);
calc()
可用于允许长度、频率、角度、时间、百分比、数字或整数的任何位置。 查看 CSS 函数的 CSS 指南,以获取完整概述。
更强大的是,你可以将 calc()
与 CSS 自定义属性结合使用 - 这是预处理器无法处理的。
时钟的指针

让我们首先使用一些自定义属性和模拟时钟指针的动画定义来奠定基础
:root {
--second: 1s;
--minute: calc(var(--second) * 60);
--hour: calc(var(--minute) * 60);
}
@keyframes rotate {
from { transform: rotate(0); }
to { transform: rotate(1turn); }
}
一切从根上下文中的 --second
自定义属性开始,我们定义了秒应该等于一秒(1s
)。 所有未来的值和时间都会从此属性派生。
此属性实质上是时钟的核心,控制着时钟所有指针的快慢。 将 --second
设置为 1s
使时钟与现实时间相符,但我们可以将其设置为 2s
来使时钟速度减半,甚至可以将其设置为 10ms
来使时钟速度快 100 倍。
我们计算的第一个属性是 --minute
指针,我们希望它等于 60 乘以一秒。 我们可以借助 calc()
来引用 --second
属性的值并将其乘以 60
--minute: calc(var(--second) * 60);
--hour
指针属性是使用完全相同的原理定义的,但乘以 --minute
指针的值
--hour: calc(var(--minute) * 60);
我们希望时钟上的所有三个指针都从 0 度旋转到 360 度 - 围绕时钟表面的形状! 三个动画之间的区别在于每个动画完成一圈所需的时间。 我们不使用 360deg
作为我们的全圆值,而是可以使用完全有效的 CSS 值 1turn
。
@keyframes rotate {
from { transform: rotate(0); }
to { transform: rotate(1turn); }
}
这些 @keyframes
只是告诉浏览器在动画期间将元素旋转一圈。 我们定义了一个名为 rotate
的动画,它现在已准备好分配给时钟的秒针
.second.hand {
animation: rotate steps(60) var(--minute) infinite;
}
我们使用 animation
简写属性来定义动画的细节。 我们添加了动画的名称(rotate
)、我们希望动画运行的时间(var(--minute)
或 60 秒)以及运行动画的次数(infinite
,表示它永远不会停止运行)。 steps(60)
是动画时序函数,它告诉浏览器以 60 个相等步骤执行 1 旋转动画。 这样,秒针会在每秒钟跳动,而不是沿着圆形平滑地旋转。
在讨论 CSS 动画时,如果我们希望动画稍后开始,可以定义动画延迟(animation-delay
),并且可以使用 animation-direction
来更改动画是向前播放还是向后播放。 我们甚至可以使用 animation-play-state
来暂停和重新启动动画。
分针和时针上的动画与秒针上的动画非常相似。 区别在于这里不需要多个步骤 - 这些指针可以以平滑的线性方式旋转。
分针需要一小时才能完成一圈,所以
.minute.hand {
animation: rotate linear var(--hour) infinite;
}
另一方面(双关语),时针需要十二小时才能绕时钟走一圈。 我们没有像 --half-day
这样的单独的自定义属性来表示这段时间,因此我们将 --hour
乘以十二
.hour.hand {
animation: rotate linear calc(var(--hour) * 12) infinite;
}
你现在可能已经了解了时钟指针的工作原理。 但是,如果我们没有真正构建时钟,它将不是一个完整的示例。
时钟表盘
到目前为止,我们只关注时钟的 CSS 方面。 我们还需要一些 HTML 才能使所有这些工作。 以下是我正在使用的内容
<main>
<div class="clock">
<div class="second hand"></div>
<div class="minute hand"></div>
<div class="hour hand"></div>
</div>
</main>
让我们看看我们必须做些什么来设计我们的时钟
.clock {
width: 300px;
height: 300px;
border-radius: 50%;
background-color: var(--grey);
margin: 0 auto;
position: relative;
}
我们使时钟高度和宽度为 300 像素,使背景颜色为灰色(使用自定义属性 --grey
,我们可以稍后定义),并使用 50% 的边框半径将其变成一个圆形。
时钟上有三个指针。 让我们首先使用绝对定位将这些指针移到时钟表盘的中心
.hand {
position: absolute;
left: 50%;
top: 50%;
}
请注意此类的名称(.hands
),因为所有三个指针都使用它作为其基本样式。 这很重要,因为对该类的任何更改都将应用于所有三个指针。
让我们定义指针的尺寸并对其进行着色
.hand {
position: absolute;
left: 50%;
top: 50%;
width: 10px;
height: 150px;
background-color: var(--blue);
}
指针现在都已到位

获取正确的旋转
让我们稍等一下,先别急着庆祝。 我们有一些问题,当指针这么细的时候,它们可能并不明显。 如果我们将指针改为 100 像素宽,会怎么样

现在我们可以看到,如果元素从左侧定位 50%,它将与父元素的中心对齐 - 但这不是我们真正需要的。 为了解决这个问题,左侧坐标需要是 50%,减去指针宽度的一半,在本例中是 50 像素

使用 calc()
函数轻松处理多个不同的度量
.hand {
position: absolute;
left: calc(50% - 50px);
top: 50%;
width: 100px;
height: 150px;
background-color: var(--grey);
}
这解决了我们的初始定位问题,但是,如果我们尝试旋转元素,我们可以看到变换原点(即旋转的支点)位于元素的中心

我们可以使用 transform-origin
属性将旋转原点更改为位于 x 轴的中心和 y 轴的顶部
.hand {
position: absolute;
left: calc(50% - 50px);
top: 50%;
width: 100px;
height: 150px;
background-color: var(--grey);
transform-origin: center 0;
}
这很棒,但并不完美,因为我们时钟指针的像素值是硬编码的。 如果我们希望指针具有不同的宽度和高度,并随时钟的实际大小而缩放,会怎么样? 你猜对了:我们需要一些 CSS 自定义属性!
.second {
--width: 5px;
--height: 140px;
--color: var(--yellow);
}
.minute {
--width: 10px;
--height: 90px;
--color: var(--blue);
}
.hour {
--width: 10px;
--height: 50px;
--color: var(--dark-blue);
}
有了这个,我们为每个指针定义了自定义属性。 这里有趣的是,我们给这些属性起了相同的名称:--width
、--height
和 --color
。 我们如何能给他们不同的值,但他们不会互相覆盖? 而且,如果我调用 var(--width)
、var(--height)
或 var(--color)
,我会得到什么值?
让我们看看时针
<div class="hour hand"></div>
我们为 .hour
类分配了新的自定义属性,它们 在本地作用于该元素,包括该元素及其所有子元素。 这意味着应用于该元素(或其访问自定义属性的子元素)的任何 CSS 样式都将看到并使用在其自身作用域中设置的特定值。 因此,如果你在时针元素或其内部的任何祖先元素中调用 var(--width)
,则从上面示例中返回的值为 10 像素。 这也意味着,如果我们尝试在这些元素外部使用这些属性中的任何一个 - 例如在 body 元素中 - 它们对于作用域之外的那些元素是不可访问的。
继续进行秒针和分针,我们进入了不同的作用域。 这意味着自定义属性可以使用不同的值重新定义。
.second {
--width: 5px;
--height: 140px;
--color: var(--yellow);
}
.minute {
--width: 10px;
--height: 90px;
--color: var(--blue);
}
.hour {
--width: 10px;
--height: 50px;
--color: var(--dark-blue);
}
.hand {
position: absolute;
top: 50%;
left: calc(50% - var(--width) / 2);
width: var(--width);
height: var(--height);
background-color: var(--color);
transform-origin: center 0;
}
最棒的是,.hand
类(我们将其分配给所有三个指针元素)可以引用这些属性进行计算和声明。 而且每个指针都会从其自身的作用域中接收其自身的属性。 在某种程度上,我们正在为每个元素个性化 .hand
类,以避免代码中不必要的重复。
我们的时钟已经启动并运行,所有指针都以正确的速度移动

我们可以到此为止,但让我提出一些改进建议。时钟上的指针从 6 点开始,但我们可以通过将时钟旋转 180 度将其初始位置设置为 12 点。让我们将此行添加到 .clock
类中
.clock {
/* same as before */
transform: rotate(180deg);
}
指针带点圆角看起来可能更美观
.hand {
/* same as before */
border-radius: calc(var(--width) / 2);
}
我们的时钟外观和工作都很好!而且所有指针都从 12 点开始,完全是我们想要的样子!
设置时钟
即使拥有所有这些很棒的功能,时钟仍然不可用,因为 **它在显示实际时间方面表现糟糕透顶**。然而,我们能做的事情有一些硬性限制。通过 HTML 和 CSS 访问本地时间以自动设置我们的时钟根本不可能。但我们可以为手动设置做好准备。
我们可以将时钟设置为从某个小时和分钟开始,如果我们在完全相同的时间运行 HTML,它将在之后准确地保持时间。这基本上是设置现实世界中时钟的方式,所以我认为这是一个可接受的解决方案。😅
让我们将我们想要启动时钟的时间添加为 .clock
类中的自定义属性
.clock {
--setTimeHour: 16;
--setTimeMinute: 20;
/* same as before */
}
我写这篇文章时,现在的时间是 16:20(或下午 4:20),因此时钟将设置为该时间。我需要做的就是在那 16:20 刷新页面,它就会准确地保持时间。
好吧,但是…… _如果 CSS 动画已经控制着旋转,我们如何将时间设置为这些位置并旋转指针?_
理想情况下,我们希望在动画从一开始就启动时,将时钟的指针旋转并设置为特定位置。假设您想将时针设置为 90 度,使其从下午 3:00 开始,并从该位置初始化旋转动画
/* this will not work */
.hour.hand {
transform: rotate(90deg);
animation: rotate linear var(--hour) infinite;
}
嗯,不幸的是,这行不通,因为动画会立即覆盖 transform
,因为它修改的是相同的 transform
属性。因此,无论我们将其设置为多少,它都将返回到动画第一个关键帧开始的 0 度。
我们可以独立于动画设置指针的旋转。例如,我们可以将指针包装到一个额外的元素中。这个额外的父元素,“设置器”,将负责设置初始位置,然后里面的指针元素可以独立地从 0 度动画到 360 度。然后,起始 0 度位置将相对于我们设置的父级设置器元素。
这将起作用,但幸运的是,有一个更好的选择! **让我让你大吃一惊!** 🪄✨✨
animation-delay
属性是我们通常用来以某些预定义的延迟启动动画的属性。
诀窍是 **使用负值**,它会立即启动动画,但从动画时间轴上的特定点开始!
要从动画开始 10 秒后开始
animation-delay: -10s;
在时针和分针的情况下,我们需要的延迟实际值是根据 --setTimeHour
和 --setTimeMinute
属性计算的。为了帮助计算,让我们创建两个新的属性,包含我们需要的偏移时间量
- 时针需要设置为我们想要设置的时钟小时数,乘以一个小时。
- 分针将我们想要设置的时钟分钟数乘以一分钟进行偏移。
--setTimeHour: 16;
--setTimeMinute: 20;
--timeShiftHour: calc(var(--setTimeHour) * var(--hour));
--timeShiftMinute: calc(var(--setTimeMinute) * var(--minute));
这些新属性包含 animation-delay
属性设置时钟所需的精确时间量。让我们将这些添加到我们的动画定义中
.second.hand {
animation: rotate steps(60) var(--minute) infinite;
}
.minute.hand {
animation: rotate linear var(--hour) infinite;
animation-delay: calc(var(--timeShiftMinute) * -1);
}
.hour.hand {
animation: rotate linear calc(var(--hour) * 12) infinite;
animation-delay: calc(var(--timeShiftHour) * -1);
}
注意,我们如何将这些值乘以 -1 以将其转换为负数。
我们几乎已经达到了完美,但有一个小问题:如果我们将分钟数设置为 30,例如,时针需要已经移动到下一小时的一半。更糟糕的情况是将分钟数设置为 59,而时针仍然处于小时的开头。
要解决这个问题,我们只需要将分针偏移和时针偏移值加在一起,用于时针
.hour.hand {
animation: rotate linear calc(var(--hour) * 12) infinite;
animation-delay: calc(
(var(--timeShiftHour) + var(--timeShiftMinute)) * -1
);
}
我认为我们已经修复了所有问题。让我们欣赏我们美丽的、纯粹的 CSS、可设置的、模拟时钟

让我们转到数字时钟
原则上,模拟时钟和数字时钟都使用相同的计算,区别在于计算的视觉表示。

我的想法是:我们可以通过设置数字的高、垂直列来创建一个数字时钟,并对这些数字进行动画处理,而不是旋转时钟指针。从时钟容器的最终版本中删除溢出遮罩,就可以揭示其中的奥秘

新的 HTML 标记需要包含所有三个时钟部分的所有数字,从秒和分钟部分的 00 到 59,以及从小时部分的 00 到 23
<main>
<div class="clock">
<div class="hour section">
<ul>
<li>00</li>
<li>01</li>
<!-- etc. -->
<li>22</li>
<li>23</li>
</ul>
</div>
<div class="minute section">
<ul>
<li>00</li>
<li>01</li>
<!-- etc. -->
<li>58</li>
<li>59</li>
</ul>
</div>
<div class="second section">
<ul>
<li>00</li>
<li>01</li>
<!-- etc. -->
<li>58</li>
<li>59</li>
</ul>
</div>
</div>
</main>
为了使这些数字对齐,我们需要编写一些 CSS,但要开始样式设置,我们可以直接从模拟时钟的 :root
范围复制自定义属性,因为时间是通用的
:root {
--second: 1s;
--minute: calc(var(--second) * 60);
--hour: calc(var(--minute) * 60);
}
最外面的包装元素 .clock
仍然具有设置初始时间的完全相同的自定义属性。唯一改变的是它变成了一个 flexbox 容器
.clock {
--setTimeHour: 14;
--setTimeMinute: 01;
--timeShiftHour: calc(var(--setTimeHour) * var(--hour));
--timeShiftMinute: calc(var(--setTimeMinute) * var(--minute));
width: 150px;
height: 50px;
background-color: var(--grey);
margin: 0 auto;
position: relative;
display: flex;
}
三个无序列表及其内部包含数字的列表项不需要任何特殊处理。如果它们的水平空间有限,数字将相互叠加。让我们确保没有列表样式来防止项目符号点,并且我们将事物居中以确保一致的位置
.section > ul {
list-style: none;
margin: 0;
padding: 0;
}
.section > ul > li {
width: 50px;
height: 50px;
font-size: 32px;
text-align: center;
padding-top: 2px;
}
布局完成了!

每个无序列表的外部是一个带有 .section
类的 <div>
。这是包装每个部分的元素,因此我们可以将其用作遮罩,以隐藏落在时钟可见区域之外的数字
.section {
position: relative;
width: calc(100% / 3);
overflow: hidden;
}
时钟的结构已完成,现在轨道已经准备好进行动画处理。
为数字时钟设置动画
整个动画的基本思路是,三条数字带可以在它们的遮罩内上下移动,以显示秒和分钟从 0 到 59 的不同数字,以及小时从 0 到 23 的不同数字(对于 24 小时制)。
我们可以通过更改数字条的 CSS 中的 translateY
过渡函数从 0 到 -100% 来实现这一点。这是因为 y 轴上的 100% 代表了整个条带的高度。值 0% 将显示当前条带的第一个数字,而 100% 将显示最后一个数字。
以前,我们的动画是基于将指针从 0 度旋转到 360 度。现在我们有了不同的动画,它将数字带从 y 轴上的 0 移动到 -100%
@keyframes tick {
from { transform: translateY(0); }
to { transform: translateY(-100%); }
}
将此动画应用于秒数字带可以与模拟时钟一样进行。唯一的区别是选择器和引用的动画名称
.second > ul {
animation: tick steps(60) var(--minute) infinite;
}
使用 step(60)
设置,动画会在数字之间进行切换,就像我们在模拟时钟的秒针上所做的那样。我们可以将其更改为 ease
,然后数字将像在纸带上一样平滑地上下滑动。
将新的滴答动画分配给分钟和小时部分同样简单
.minute > ul {
animation: tick steps(60) var(--hour) infinite;
animation-delay: calc(var(--timeShiftMinute) * -1);
}
.hour > ul {
animation: tick steps(24) calc(24 * var(--hour)) infinite;
animation-delay: calc(var(--timeShiftHour) * -1);
}
同样,声明非常相似,这次不同的是选择器、时间函数和动画名称。
时钟现在滴答作响并保持正确的时间

:
)
还有一个细节:闪烁的冒号 (我们也可以到此为止,然后结束一天。但还有一件事我们可以做,让我们的数字时钟更逼真:让分钟和秒之间的冒号分隔符在每秒过去时闪烁。
我们可以在 HTML 中添加这些冒号,但它们不是内容的一部分。我们希望它们成为时钟外观和样式的增强功能,因此 CSS 是存储此内容的正确位置。这就是 content
属性的作用,我们可以将其用于分钟和小时的 ::after
伪元素
.minute::after,
.hour::after {
content: ":";
margin-left: 2px;
position: absolute;
top: 6px;
left: 44px;
font-size: 24px;
}
太简单了!但我们如何让秒冒号也闪烁?我们希望它有动画效果,所以我们需要定义一个新的动画?有许多方法可以实现这一点,但我认为我们应该这次更改 content
属性,以证明,出乎意料的是,在动画过程中可以更改其值
@keyframes blink {
from { content: ":"; }
to { content: ""; }
}
在所有浏览器中,为 content
属性设置动画都不起作用,因此您可以将其更改为 opacity
或 visibility
作为安全选择……
最后一步是将闪烁动画分配给分钟部分的伪元素
.minute::after {
animation: blink var(--second) infinite;
}
**就这样,我们完成了!** 数字时钟准确地保持时间,我们甚至设法在数字之间添加了一个闪烁的分隔符。
书籍:你只需要 HTML 和 CSS

这只是一个来自我的新书《你只需要 HTML 和 CSS》的示例项目。它在亚马逊的 美国 和 英国 均有售。
如果你刚开始学习网页开发,书中的理念将帮助你提升水平,构建交互式、动画化的网页界面,而无需接触任何 JavaScript。
如果你是一名经验丰富的 JavaScript 开发人员,这本书可以很好地提醒你,许多事情都可以只用 HTML 和 CSS 来构建,尤其是在我们现在拥有更多强大的 CSS 工具和功能的情况下,比如我们在本文中介绍的那些。书中有很多例子突破了极限,包括交互式轮播、手风琴、计算、计数、高级输入验证、状态管理、可关闭的模态窗口,以及对鼠标和键盘输入的反应。甚至还有完整的星级评分小部件和购物车。
感谢你花时间和我一起构建时钟!
这篇文章如果有一个嵌入式的可运行示例会更好,这样你就可以立即进行尝试。
“用 HTML 和 CSS 访问本地时间来自动设置我们的时钟是不可能的。”
所以标题是谎言吗?
下一行字面意思是说它是用 CSS 手动完成的。标题从未说过它是一个自动的东西。这怎么算谎言呢?
所以,这不是一个“仅使用 CSS 的时钟,它显示当前时间”。它要么是仅使用 CSS 的时钟,要么是显示当前时间的时钟,两者不能兼得。这个标题感觉不真诚。
哪一部分?这个时钟是仅使用 CSS 的,并且手动设置为当前时间。因此,这是一个仅使用 CSS 的时钟,它显示当前时间。
哈,该死,我不是第一个提出这个观点的人。但是……这感觉有点像标题党。“当前时间”后面有一个巨大的星号。也许我应该在后面加上一个简单的 JS 调用,可以启动你的时钟。重点是,我们有很多 UX 可以用纯粹的 GPU 驱动逻辑来实现,我们不需要庞大的 JS 框架。也许只需要一点
onload=""
就可以。Geoff 是对的。文章中没有任何误导性或不真诚的地方。试着按照文章的写作方式阅读。作者在他的解释中非常清晰。
你可以让手表从当前时间开始,方法是在 HTML 中内联起始值,并使用 PHP 用当前时间填充它们——脚本执行时间偏移量,这样应该可以在不使用 JS 的情况下实现。不知道这是否算得上 100% 的 CSS。
有没有办法衡量运行 CSS 动画的性能,或者有没有办法改进长时间运行的动画?这里,时针动画是长时间运行的,虽然性能影响并不直接可见,但在尝试打开动画开发者工具并尝试分析动画时就会很明显。
性能可以通过使用 JS 来提高,但如果有 CSS 的方法,我很想知道。:)
朋友!这出现在我的推荐列表中。很高兴看到你制作演示和内容。语言的清晰度也很棒。
继续努力。
blah blah blah,误导性与否,你可以用服务器端脚本将当前时间戳直接放入样式属性的变量中。当然,你可能还想添加一个 JS 备用方案,但这已经超出了仅使用 CSS 的范围。
CSS 是唯一需要的客户端代码,所以我觉得 Mate 应该添加一个基本的例子,让每个人都能轻松使用。
作者是正确的,文章中没有任何误导性或不真诚的地方,但这个标题会吸引许多想要知道 CSS 如何访问当前时间的人。而它做不到。所以标题是有点击诱导性的。这就是人们在这里争论的焦点。
除此之外,这是一篇非常有见地的文章,有很多实用的内容,可以应用于很多方面和不同的场景。我非常喜欢阅读这篇文章。
很棒,简洁,而且很酷,感谢分享。
如果标题误导了大家,我表示歉意‘:D也许标题应该说“仅使用 CSS 的时钟,它保持当前时间”,而不是“……显示当前时间”。
无论如何,正如我们都知道的,CSS 和 HTML 无法访问计算机的本地时间。我不喜欢 PHP 解决方案,并且不允许使用 JS。
我认为,我们所能做的最好的事情是用最语义化的方式用
<time>
元素和datetime=""
属性在 HTML 中定义当前时间。这比在样式属性中添加自定义属性要好得多。接下来,我们只需要根据
datetime
属性中的时间设置--setTimeHour
和--setTimeMinute
的值。如果我们对 24 小时和 60 分钟值重复此操作,它将完美地工作。我们可以很容易地借助 SCSS 来完成。
这里有一个快速演示供你参考。
请告诉我你们的想法。:)
人们可以变得非常有创意,但在这种情况下,由于它最终是一个图形,我觉得不错,但我更喜欢使用 SVG 在浏览器上绘制。
如果你像我一样一边阅读一边输入文章中的代码,屏幕上什么都不会显示。为什么?因为作者从未真正定义过颜色。这是一个如此简单的小细节,对于文章来说是必不可少的,但却完全毁了它。我想说“很棒的文章!”,但我真的说不出口。