您是否曾经在项目中需要一个倒计时器?对于类似的事情,使用插件可能是自然的选择,但实际上,创建一个倒计时器比您想象的要简单得多,只需要 HTML、CSS 和 JavaScript 三剑客即可。让我们一起创建一个吧!
这是我们的目标
以下是我们在本文中将介绍的计时器的一些功能
- 显示剩余的初始时间
- 将时间值转换为
MM:SS
格式 - 计算剩余的初始时间与已过去时间的差值
- 随着剩余时间接近零,颜色发生变化
- 将剩余时间的进度显示为动画环
好的,这就是我们想要的,所以让我们开始吧!
步骤 1:从基本的标记和样式开始
让我们从为我们的计时器创建一个基本模板开始。我们将添加一个带有 svg 元素的 circle,以绘制一个指示时间流逝的计时器环,并添加一个 span 来显示剩余的时间值。请注意,我们正在 JavaScript 中编写 HTML 并将其注入 DOM,方法是定位 #app
元素。当然,如果更符合您的习惯,我们可以将其中许多内容移动到 HTML 文件中。
document.getElementById("app").innerHTML = `
<div class="base-timer">
<svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<g class="base-timer__circle">
<circle class="base-timer__path-elapsed" cx="50" cy="50" r="45" />
</g>
</svg>
<span>
<!-- Remaining time label -->
</span>
</div>
`;
现在我们有一些标记可以使用了,让我们对其进行一些样式设置,以便我们有一个良好的视觉起点。具体来说,我们将
- 设置计时器的大小
- 删除圆形包装元素的填充和描边,以便我们获得形状,但让经过的时间显示出来
- 设置环的宽度和颜色
/* Sets the containers height and width */
.base-timer {
position: relative;
height: 300px;
width: 300px;
}
/* Removes SVG styling that would hide the time label */
.base-timer__circle {
fill: none;
stroke: none;
}
/* The SVG path that displays the timer's progress */
.base-timer__path-elapsed {
stroke-width: 7px;
stroke: grey;
}
完成这些操作后,我们将得到一个如下所示的基本模板。

步骤 2:设置时间标签
您可能已经注意到,模板包含一个空的 <span>,它将保存剩余的时间。我们将用适当的值填充该位置。我们之前说过,时间将采用 MM:SS
格式。为此,我们将创建一个名为 formatTimeLeft
的方法
function formatTimeLeft(time) {
// The largest round integer less than or equal to the result of time divided being by 60.
const minutes = Math.floor(time / 60);
// Seconds are the remainder of the time divided by 60 (modulus operator)
let seconds = time % 60;
// If the value of seconds is less than 10, then display seconds with a leading zero
if (seconds < 10) {
seconds = `0${seconds}`;
}
// The output in MM:SS format
return `${minutes}:${seconds}`;
}
然后,我们将在模板中使用我们的方法
document.getElementById("app").innerHTML = `
<div class="base-timer">
<svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<g class="base-timer__circle">
<circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>
</g>
</svg>
<span id="base-timer-label" class="base-timer__label">
${formatTime(timeLeft)}
</span>
</div>
`
为了在环内显示值,我们需要稍微更新一下我们的样式。
.base-timer__label {
position: absolute;
/* Size should match the parent container */
width: 300px;
height: 300px;
/* Keep the label aligned to the top */
top: 0;
/* Create a flexible box that centers content vertically and horizontally */
display: flex;
align-items: center;
justify-content: center;
/* Sort of an arbitrary number; adjust to your liking */
font-size: 48px;
}
好的,我们准备使用 timeLeft
值了,但该值尚不存在。让我们创建它并将初始值设置为我们的时间限制。
// Start with an initial value of 20 seconds
const TIME_LIMIT = 20;
// Initially, no time has passed, but this will count up
// and subtract from the TIME_LIMIT
let timePassed = 0;
let timeLeft = TIME_LIMIT;
我们又近了一步。

没错!现在我们有一个从 20 秒开始的计时器……但它还没有进行任何计数。让我们让它动起来,使其倒计时到零秒。
步骤 3:倒计时
让我们考虑一下我们需要做什么才能倒计时。现在,我们有一个 timeLimit
值,它表示我们的初始时间,以及一个 timePassed
值,它指示倒计时开始后经过了多长时间。
我们需要做的是每秒将 timePassed
值增加一个单位,并根据新的 timePassed
值重新计算 timeLeft
值。我们可以使用 setInterval
函数来实现这一点。
让我们实现一个名为 startTimer
的方法,该方法将
- 设置计数器间隔
- 每秒递增
timePassed
值 - 重新计算
timeLeft
的新值 - 更新模板中的标签值
我们还需要保留对该间隔对象的引用,以便在需要时清除它——这就是我们将创建一个 timerInterval
变量的原因。
let timerInterval = null;
document.getElementById("app").innerHTML = `...`
function startTimer() {
timerInterval = setInterval(() => {
// The amount of time passed increments by one
timePassed = timePassed += 1;
timeLeft = TIME_LIMIT - timePassed;
// The time left label is updated
document.getElementById("base-timer-label").innerHTML = formatTime(timeLeft);
}, 1000);
}
我们有一个启动计时器的方法,但我们没有在任何地方调用它。让我们在加载时立即启动我们的计时器。
document.getElementById("app").innerHTML = `...`
startTimer();
就是这样!我们的计时器现在将倒计时。虽然这很棒,但如果我们可以在时间标签周围的环上添加一些颜色,并在不同的时间值时更改颜色,那就更好了。

步骤 4:用另一个环覆盖计时器环
为了可视化时间流逝,我们需要在我们的环上添加一个第二层来处理动画。我们所做的本质上是在原始灰色环的顶部叠加一个新的绿色环,以便绿色环随着时间的推移进行动画以显示灰色环,就像进度条一样。
让我们首先在我们的 SVG 元素中添加一个 path 元素。
document.getElementById("app").innerHTML = `
<div class="base-timer">
<svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<g class="base-timer__circle">
<circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>
<path
id="base-timer-path-remaining"
stroke-dasharray="283"
class="base-timer__path-remaining ${remainingPathColor}"
d="
M 50, 50
m -45, 0
a 45,45 0 1,0 90,0
a 45,45 0 1,0 -90,0
"
></path>
</g>
</svg>
<span id="base-timer-label" class="base-timer__label">
${formatTime(timeLeft)}
</span>
</div>
`;
接下来,让我们为剩余时间路径创建一个初始颜色。
const COLOR_CODES = {
info: {
color: "green"
}
};
let remainingPathColor = COLOR_CODES.info.color;
最后,让我们添加一些样式,使圆形路径看起来像我们最初的灰色环。这里重要的是要确保 stroke-width
与原始环的大小相同,并且 transition
的持续时间设置为一秒,以便它平滑地进行动画并与时间标签中的剩余时间相对应。
.base-timer__path-remaining {
/* Just as thick as the original ring */
stroke-width: 7px;
/* Rounds the line endings to create a seamless circle */
stroke-linecap: round;
/* Makes sure the animation starts at the top of the circle */
transform: rotate(90deg);
transform-origin: center;
/* One second aligns with the speed of the countdown timer */
transition: 1s linear all;
/* Allows the ring to change color when the color value updates */
stroke: currentColor;
}
.base-timer__svg {
/* Flips the svg and makes the animation to move left-to-right */
transform: scaleX(-1);
}
这将输出一个覆盖计时器环的描边,就像它应该的那样,但它还没有进行动画以随着时间的推移显示计时器环。

为了动画化剩余时间线的长度,我们将使用 stroke-dasharray
属性。Chris 解释了如何使用它来创建元素“绘制”自身的错觉。并且在 CSS-Tricks 年鉴中 详细介绍了该属性 及其示例。
步骤 5:动画化进度环
让我们看看我们的环在使用不同的 stroke-dasharray
值时会是什么样子

我们可以看到,stroke-dasharray
的值实际上将我们的剩余时间环分割成等长部分,其中长度为剩余时间值。当我们将 stroke-dasharray
的值设置为一位数(即 1-9)时,就会发生这种情况。
名称 dasharray 表明我们可以将多个值设置为数组。让我们看看如果我们设置两个数字而不是一个数字,它会如何表现;在这种情况下,这些值为 10 和 30。

stroke-dasharray: 10 30
这将第一部分(剩余时间)长度设置为 10,第二部分(已过去时间)长度设置为 30。我们可以通过一个小技巧在我们的计时器中使用它。我们最初需要的是让环覆盖圆的整个长度,这意味着剩余时间等于我们环的长度。
那个长度是多少?拿出你旧的几何教科书,因为我们可以用一些数学来计算弧长
Length = 2πr = 2 * π * 45 = 282,6
这就是我们想要在环最初挂载时使用的值。让我们看看它是什么样子。

stroke-dasharray: 283 283
这有效!
好的,数组中的第一个值是我们的剩余时间,第二个值标记已过去的时间。我们现在需要做的是操作第一个值。让我们在下面看看当我们更改第一个值时可以预期什么。

我们将创建两个方法,一个负责计算剩余初始时间的几分之几,另一个负责计算 stroke-dasharray
值并更新表示我们剩余时间的 <path>
元素。
// Divides time left by the defined time limit.
function calculateTimeFraction() {
return timeLeft / TIME_LIMIT;
}
// Update the dasharray value as time passes, starting with 283
function setCircleDasharray() {
const circleDasharray = `${(
calculateTimeFraction() * FULL_DASH_ARRAY
).toFixed(0)} 283`;
document
.getElementById("base-timer-path-remaining")
.setAttribute("stroke-dasharray", circleDasharray);
}
我们还需要在每秒经过时更新我们的路径。这意味着我们需要在我们的 timerInterval
中调用新创建的 setCircleDasharray
方法。
function startTimer() {
timerInterval = setInterval(() => {
timePassed = timePassed += 1;
timeLeft = TIME_LIMIT - timePassed;
document.getElementById("base-timer-label").innerHTML = formatTime(timeLeft);
setCircleDasharray();
}, 1000);
}
现在我们可以看到事物在移动了!

哇哦,它起作用了……但是……仔细看看,尤其是在最后。看起来我们的动画延迟了一秒。当我们达到 0 时,仍然可以看到一小部分环。

这是因为动画持续时间设置为一秒。当剩余时间值设置为零时,它仍然需要一秒才能真正将环动画设置为零。我们可以通过在倒计时期间逐渐减小环的长度来消除这种情况。我们在 calculateTimeFraction
方法中执行此操作。
function calculateTimeFraction() {
const rawTimeFraction = timeLeft / TIME_LIMIT;
return rawTimeFraction - (1 / TIME_LIMIT) * (1 - rawTimeFraction);
}
就是这样!

糟糕……还有一件事。我们说我们想在剩余时间达到某些点时更改进度指示器的颜色——有点像让用户知道时间快到了。
步骤 6:在特定时间点更改进度颜色
首先,我们需要添加两个阈值,用于指示何时应更改为警告和警报状态,并为每个状态添加颜色。我们从绿色开始,然后变为橙色作为警告,当时间即将用完时变为红色。
// Warning occurs at 10s
const WARNING_THRESHOLD = 10;
// Alert occurs at 5s
const ALERT_THRESHOLD = 5;
const COLOR_CODES = {
info: {
color: "green"
},
warning: {
color: "orange",
threshold: WARNING_THRESHOLD
},
alert: {
color: "red",
threshold: ALERT_THRESHOLD
}
};
现在,让我们创建一个负责检查阈值是否超过并更改进度颜色的方法。
function setRemainingPathColor(timeLeft) {
const { alert, warning, info } = COLOR_CODES;
// If the remaining time is less than or equal to 5, remove the "warning" class and apply the "alert" class.
if (timeLeft <= alert.threshold) {
document
.getElementById("base-timer-path-remaining")
.classList.remove(warning.color);
document
.getElementById("base-timer-path-remaining")
.classList.add(alert.color);
// If the remaining time is less than or equal to 10, remove the base color and apply the "warning" class.
} else if (timeLeft <= warning.threshold) {
document
.getElementById("base-timer-path-remaining")
.classList.remove(info.color);
document
.getElementById("base-timer-path-remaining")
.classList.add(warning.color);
}
}
因此,我们基本上是在计时器到达某个点时删除一个 CSS 类,并将其替换为另一个类。我们需要定义这些类。
.base-timer__path-remaining.green {
color: rgb(65, 184, 131);
}
.base-timer__path-remaining.orange {
color: orange;
}
.base-timer__path-remaining.red {
color: red;
}
瞧,就是这样。以下是将所有内容组合在一起的演示。
相关……陈慧静的 你能用纯 CSS 制作倒计时器吗?
这里有一个 基于本文中内容的另一个快速的纯 CSS 版本。
更改
通过使两个元素都围绕
0,0
点绘制圆圈来简化 SVG(将viewBox
设置为-50 -50 100 100
会将原点0,0
放在 SVG 的正中间,并且我们不再需要设置cx
和cy
,因为它们都默认为0
)使用
linear
关键帧动画同时为stroke-dashoffset
和stroke
设置动画使用
steps()
关键帧动画为计时器内容设置动画(如果我们要使用 Houdini,这部分可以简化很多,但那样会以牺牲支持为代价,所以我不知道)如果它可以点击按钮或在完成时自动转到特定 URL,那就太好了。
如何使计时器填充而不是倒计时?我想从这个示例中获取样本以制作一个小音乐播放器
你可以用
FULL_DASH_ARRAY - (calculateTimeFraction() * FULL_DASH_ARRAY)
替换calculateTimeFraction() * FULL_DASH_ARRAY
。我们如何更改此动画的方向?
不错!我自己也玩过一段时间计时器。如果有人感兴趣,这里有一点小技巧,你可以
放弃 if 语句来显示小于 10 的时间并使用前导 0,可以使用 padStart。像这样
返回
${minutes}:${seconds.toString().padStart(2, '0')}
;我认为这将是转换为自定义元素的一个很好的候选者,所以我尝试了一下。我快完成了,但究竟是什么
https://codepen.io/amundo/pen/PoqYWEK
这是一篇非常酷、写得很好的文章!
仍然存在一个小错误,即“环”似乎直到倒计时达到 00:19 才开始移动。
是否可以根据工作日计算计时器?
我建议稍微修改一下教程,因为现在在遵循教程时缺少一些步骤,例如 startTimer() 函数中的条件(因此计时器永远不会停止并变为负值)或设置 FULL_DASH_ARRAY 常量。
这个倒计时时钟非常漂亮,非常适合台球游戏。
需要能够启动、停止/暂停、恢复,在我所见过的称为滞后的情况下,每次启动和停止都报告时间长度并提供剩余时间。这很酷,可能性……
这看起来很棒!调整它使其显示
小时:分钟:秒
// 复制分钟并将其称为小时。在返回时添加小时值。搞定!
function formatTime(time) {
const hours = Math.floor(time / 60);
const minutes = Math.floor(time / 60);
let seconds = time % 60;
if (seconds < 10) {
seconds = `0${seconds}`;
}
return `${hours}:${minutes}:${seconds}`;
}
步骤 2 中的方法名称与步骤 2 中模板中的方法名称不匹配。
formatTimeLeft / formatTime
很棒的教程!
嗨,喜欢这个计时器
我需要在一个页面上使用多个计时器,并且每个计时器都通过点击操作启动。
当我尝试时,我得到 JS 错误“FULL_DASH_ARRAY”已被声明,加上我在每个计时器实例中添加的其他常量变量 _1、_2、_3 等“FULL_DASH_ARRAY_1”,但我得到了同样的问题。
有什么想法吗?
在 Ie8 上,不知何故此动画无法正常工作。请建议
很棒的东西。喜欢它。
我必须做什么才能删除分钟和冒号,当剩余不到 1 分钟时?
此外,我该如何使其在倒计时达到 00 时显示某些文本?
很棒的教程!我正在我的项目中使用它。但是,有一个小错误我无法解决。圆圈只在一两秒后开始移动。当它达到 10 秒时,圆圈应该在中间。
我发现,在第一次计算 dashArray 时,函数 calculateTimeFraction 获取 19、20 作为参数。然而,我认为它应该使用“20,20”,因为计时器刚刚启动。
const circleDashArray = `$(
calculateTimeFraction(timeLeft, timeLimit) * FULL_DASH_ARRAY
).toFixed(0)} 283`;
如何使用 React Hook 实现这种行为?
我希望这对某些人有所帮助。正如你可能注意到的,动画在一秒钟后开始移动(在最后一个示例中从 19 秒开始)。原因是 1 秒的间隔。应用程序等待 1 秒,然后开始移动。
要解决此问题,请在设置间隔之前启动动画。示例
此外,你不需要在计算时间分数时进行修复。将其保留为 timeLeft / TIME_LIMIT 可以完美工作。
谢谢,我真的很喜欢这篇文章,学到了很多!
这很漂亮,非常适合我想要为健身房计时器做的事情。
对于如何使用手机整合“遥控器”有任何想法吗?
我是一个编程新手,但我首先想到的是类似于 Web Socket 到服务器以检查计时器是否重置、停止等。这是否是我应该考虑用于此任务的方向。只是希望为研究指明方向。
你好,
感谢分步教程!我想知道,为什么 2*pi*r 公式中的“r”等于 45?
圆的宽度和高度为 300px,所以我猜半径应该为 150(直径的一半)?
请解释一下。谢谢!
嗨,由于某些原因,它似乎无法正常工作.. 首先,FULL_DASH_ARRAY 未定义,动画也不起作用
倒计时结束后,是否可以添加一个下载按钮,并在倒计时达到零后显示出来?
教程缺少 FULL_DASH_ARRAY 的定义,因此绿色进度条从未移动。
基于 stroke-dasharray 设置,我创建了变量(放置在其他 const 变量 TIME_LIMIT 下一行)。
如果您遇到此问题,则需要将以下内容添加到您的 js 中
const FULL_DASH_ARRAY = 283;
你好,
感谢您的脚本。
达到零后,当再次调用 startTimer() 函数时,如何重置它?谢谢
此致,Nuno