当然,我们可以用纯 CSS 制作一个显示当前时间的时钟!

Avatar of Mate Marschalko
Mate Marschalko

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 $200 免费积分!

让我们使用 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 自定义属性结合使用 - 这是预处理器无法处理的。

时钟的指针

Image of a round clock with a light grey background. The hours hand is dark blue pointed at 2, the minutes are light blue pointed at 9 and the seconds are yellow and pointed at 2.

让我们首先使用一些自定义属性和模拟时钟指针的动画定义来奠定基础

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

指针现在都已到位

A round clock with just one light blue hand pointing directly at 6.

获取正确的旋转

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

A round clock with a light grey background. The light blue hand is thick and still pointed at 6, but

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

Grey circle with a red vertical line bisecting it in the center. Two red arrows are on the clock, one labeled 50% pointing at the red line and another labeled negative 50 pixels pointing away from the red line.

使用 calc() 函数轻松处理多个不同的度量

.hand {
  position: absolute;
  left: calc(50% - 50px);
  top: 50%;
  width: 100px;
  height: 150px;
  background-color: var(--grey);
}

这解决了我们的初始定位问题,但是,如果我们尝试旋转元素,我们可以看到变换原点(即旋转的支点)位于元素的中心

Grey circle showing a blue rectangle on top rotated counter-clockwise.

我们可以使用 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 类,以避免代码中不必要的重复。

我们的时钟已经启动并运行,所有指针都以正确的速度移动

The finished clock. Grey background, with minutes and hours pointing to 6 and seconds pointing to 7.

我们可以到此为止,但让我提出一些改进建议。时钟上的指针从 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、可设置的、模拟时钟

让我们转到数字时钟

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

Clock with a rectangular light grey background with the numeric time reading 14 45 18 in 24-hour format.

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

新的 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);
}

同样,声明非常相似,这次不同的是选择器、时间函数和动画名称。

时钟现在滴答作响并保持正确的时间

Time reading 14 1 10 in 24-hour format.

还有一个细节:闪烁的冒号 (:)

我们也可以到此为止,然后结束一天。但还有一件事我们可以做,让我们的数字时钟更逼真:让分钟和秒之间的冒号分隔符在每秒过去时闪烁。

我们可以在 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 属性设置动画都不起作用,因此您可以将其更改为 opacityvisibility 作为安全选择……

最后一步是将闪烁动画分配给分钟部分的伪元素

.minute::after {
  animation: blink var(--second) infinite;
}

**就这样,我们完成了!** 数字时钟准确地保持时间,我们甚至设法在数字之间添加了一个闪烁的分隔符。

书籍:你只需要 HTML 和 CSS

这只是一个来自我的新书《你只需要 HTML 和 CSS》的示例项目。它在亚马逊的 美国英国 均有售。

如果你刚开始学习网页开发,书中的理念将帮助你提升水平,构建交互式、动画化的网页界面,而无需接触任何 JavaScript。

如果你是一名经验丰富的 JavaScript 开发人员,这本书可以很好地提醒你,许多事情都可以只用 HTML 和 CSS 来构建,尤其是在我们现在拥有更多强大的 CSS 工具和功能的情况下,比如我们在本文中介绍的那些。书中有很多例子突破了极限,包括交互式轮播、手风琴、计算、计数、高级输入验证、状态管理、可关闭的模态窗口,以及对鼠标和键盘输入的反应。甚至还有完整的星级评分小部件和购物车。

感谢你花时间和我一起构建时钟!