纯 CSS 码表 ( 可计时 )
这篇教学会使用非常多的 CSS 进阶技巧,包含 CSS 变数、动画控制、结构选择器、虚拟类别选择器、自订属性、清单计数器...等,透过这些技巧的互相搭配组合,就能在不使用 JavaScript 的情况下,透过纯 CSS 制作出可以计时的码表。
快速导览:
制作可以暂停和继续的数字
使用“纯 CSS”制作“可以暂停和继续”的数字需要用到许多进阶技巧,暂停和继续的控制方式是依靠“CSS 变数”和“动画暂停与继续”,数字纪录的方式依靠“自订属性”和“计数器”,最后透过虚拟元素显示数值,详细说明可参考下方范例程序码注解。
参考:CSS 变数、CSS 动画 animation、counter-set 设定清单数值、CSS @property 自订属性值、虚拟元素选择器
<!-- HTML 程式碼 -->
<div id="stopwatch">
<span class="ms"></span>
</div>
<!-- CSS 程式碼 -->
<style>
/* 自訂屬性 */
@property --ms {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
#stopwatch {
--state: paused; /* 自訂變數 --state 預設值 paused */
font-size: 100px;
}
#stopwatch:hover {
--state: running; /* hover 時改變變數 --state 為 running */
}
.ms {
counter-reset: count-ms var(--ms); /* 設定計數器名稱 count-ms,數值為 --ms 數值 */
animation: oxxo-ms 1s steps(10) var(--state) infinite; /* 使用 steps 方式播放動畫 */
}
.ms::before {
content: counter(count-ms); /* 虛擬元素顯示 count-ms 計數器數值 */
}
@-webkit-keyframes oxxo-ms {
to {
--ms: 10; /* 動畫結束點為數值為 10 的自訂屬性 --ms */
}
}
</style>
延伸上述程序码,将其改成使用 input 的 checkbox 元素,并将负责暂停和继续的变数移动到 checked 状态的选择器里,就能做到点击开始,再点一下就暂停的效果。
<!-- HTML 程式碼 -->
<div id="stopwatch">
<input type="checkbox" id="pause">
<label for="pause">點我</label>
<br>
<span class="ms"></span>
</div>
<!-- CSS 程式碼 -->
<style>
/* 自訂屬性 */
@property --ms {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
#stopwatch {
--state: paused;
font-size: 50px;
margin: 30px;
}
.ms {
counter-reset: count-ms var(--ms);
animation: oxxo-ms 1s steps(10) var(--state) infinite;
}
.ms::before {
content: counter(count-ms);
}
@-webkit-keyframes oxxo-ms {
to {
--ms: 10;
}
}
input {
display:none; /* 隱藏 checkbox,使用 label + for 連動 checkbox */
}
input:checked ~ * {
--state: running; /* checked 狀態時啟動動畫 */
}
label {
cursor: pointer;
}
label::after {
content: " ( 暫停 )"
}
input:checked ~ label::after {
content: " ( 繼續 )"
}
</style>
加入分钟数、秒数和毫秒数
延伸上述程序码,将其改成使用 input 的 checkbox 元素,并将负责暂停和继续的变数移动到 checked 状态的选择器里,就能做到点击开始,再点一下就暂停的效果。
<!-- HTML 程式碼 -->
<div id="stopwatch">
<input type="checkbox" id="pause">
<label for="pause">點我</label>
<br>
<span class="num m10"></span><span class="num m"></span>:<span class="num s10"></span><span class="num s"></span>:<span class="num ms10"></span><span class="num ms"></span>
</div>
<!-- CSS 程式碼 -->
<style>
/* 自訂屬性,分鐘十位數 */
@property --m10 {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
/* 自訂屬性,分鐘個位數 */
@property --m {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
/* 自訂屬性,秒數十位數 */
@property --s10 {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
/* 自訂屬性,秒數個位數 */
@property --s {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
/* 自訂屬性,豪秒數十位數 */
@property --ms10 {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
/* 自訂屬性,豪秒數個位數 */
@property --ms {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
#stopwatch {
--state: paused;
font-size: 50px;
margin: 30px;
font-family: monospace; /* 等寬字體 */
}
/* 把動畫樣式集中到一個類別中,透過變數就能產生不同的動畫效果
--a-name 變數表示動畫名稱,--a-time 變數表示動畫時間,--a-steps 變數表示動畫步驟 */
.num {
animation: var(--a-name) var(--a-time) steps(var(--a-steps)) var(--state) infinite;
}
/* 分鐘數十位數 */
.m10 {
counter-reset: count-m10 var(--m10);
--a-name: oxxo-m10;
--a-time: 3600s; /* 動畫持續時間為 3600 秒 */
--a-steps: 6; /* 分成六個步驟,也就是一個步驟 600 秒 = 10 分鐘 */
}
.m10::before {content: counter(count-m10);}
@-webkit-keyframes oxxo-m10 {
to {--m10: 6;}
}
/* 分鐘數個位數 */
.m {
counter-reset: count-m var(--m);
--a-name: oxxo-m;
--a-time: 600s; /* 動畫持續時間為 600 秒 */
--a-steps: 10; /* 分成十個步驟,也就是一個步驟 60 秒 = 1 分鐘 */
}
.m::before {content: counter(count-m); }
@-webkit-keyframes oxxo-m {
to {--m: 10;}
}
/* 秒數十位數 */
.s10 {
counter-reset: count-s10 var(--s10);
--a-name: oxxo-s10;
--a-time: 60s; /* 動畫持續時間為 60 秒 */
--a-steps: 6; /* 分成六個步驟,也就是一個步驟 10 秒 */
}
.s10::before {content: counter(count-s10);}
@-webkit-keyframes oxxo-s10 {
to {--s10: 6;}
}
/* 秒數個位數 */
.s {
counter-reset: count-s var(--s);
--a-name: oxxo-s;
--a-time: 10s; /* 動畫持續時間為 10 秒 */
--a-steps: 10; /* 分成十個步驟,也就是一個步驟 1 秒 */
}
.s::before {content: counter(count-s);}
@-webkit-keyframes oxxo-s {
to {--s: 10;}
}
/* 豪秒數十位數 */
.ms10 {
counter-reset: count-ms10 var(--ms10);
--a-name: oxxo-ms10;
--a-time: 1s; /* 動畫持續時間為 1 秒 */
--a-steps: 10; /* 分成十個步驟,也就是一個步驟 0.1 秒 */
}
.ms10::before {content: counter(count-ms10);}
@-webkit-keyframes oxxo-ms10 {
to {--ms10: 10;}
}
/* 豪秒數個位數 */
.ms {
counter-reset: count-ms var(--ms);
--a-name: oxxo-ms;
--a-time: 0.1s; /* 動畫持續時間為 0.1 秒 */
--a-steps: 10; /* 分成十個步驟,也就是一個步驟 0.01 秒 */
}
.ms::before {content: counter(count-ms); }
@-webkit-keyframes oxxo-ms {
to {--ms: 10;}
}
input {
display:none; /* 隱藏 checkbox,使用 label + for 連動 checkbox */
}
input:checked ~ * {
--state: running; /* checked 狀態時啟動動畫 */
}
label {
cursor: pointer;
}
label::after {
content: " ( 暫停 )"
}
input:checked ~ label::after {
content: " ( 繼續 )"
}
</style>
加入重新启动机制
如果要单纯使用 CSS 产生重新启动的机制,必须要稍微修改 HTML 程序码,修改重点如下:
- 使用
form元素包覆button和checkbox元素。- 透过
button的reset类型重置checkbox的状态。- 新增负责“开始”的
checkbox。- 修改原本“暂停/继续”的
checkbox,和开始的checkbox互相搭配。- 按下重置
button时,停止 CSS 动画。- 按下开始
checkbox时,启动 CSS 动画。- 按下暂停
checkbox时,暂停 CSS 动画。
下方范例除了按照上述重点修改程序码,也额外让开始与暂停 checkbox“只显示其中一种”,如此就能透过 active 和 ckecked 的虚拟类别,判断目前按钮状态,进而决定动画的播放状态。
<!-- HTML 程式碼 -->
<form id="stopwatch">
<button id="reset" type="reset">點我就會重置</button>
<br>
<input type="checkbox" id="start">
<label for="start" class="start">點我 ( 開始 )</label>
<input type="checkbox" id="pause">
<label for="pause" class="pause">點我</label>
<br>
<span class="num m10"></span><span class="num m"></span>:<span class="num s10"></span><span class="num s"></span>:<span class="num ms10"></span><span class="num ms"></span>
</form>
<!-- CSS 程式碼 -->
<style>
/* 自訂屬性,分鐘十位數 */
@property --m10 {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
/* 自訂屬性,分鐘個位數 */
@property --m {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
/* 自訂屬性,秒數十位數 */
@property --s10 {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
/* 自訂屬性,秒數個位數 */
@property --s {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
/* 自訂屬性,豪秒數十位數 */
@property --ms10 {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
/* 自訂屬性,豪秒數個位數 */
@property --ms {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
#stopwatch {
--state: paused;
font-size: 50px;
margin: 30px;
font-family: monospace; /* 等寬字體 */
}
/* 把動畫樣式集中到一個類別中,透過變數就能產生不同的動畫效果
透過虛擬類別判斷按下開始按 checkbox 時,才會進行動畫,否則動畫會停止
--a-name 變數表示動畫名稱,--a-time 變數表示動畫時間,--a-steps 變數表示動畫步驟 */
#start:checked ~ .num {
animation: var(--a-name) var(--a-time) steps(var(--a-steps)) var(--state) infinite;
}
/* 分鐘數十位數 */
.m10 {
counter-reset: count-m10 var(--m10);
--a-name: oxxo-m10;
--a-time: 3600s; /* 動畫持續時間為 3600 秒 */
--a-steps: 6; /* 分成六個步驟,也就是一個步驟 600 秒 = 10 分鐘 */
}
.m10::before {content: counter(count-m10);}
@-webkit-keyframes oxxo-m10 {
to {--m10: 6;}
}
/* 分鐘數個位數 */
.m {
counter-reset: count-m var(--m);
--a-name: oxxo-m;
--a-time: 600s; /* 動畫持續時間為 600 秒 */
--a-steps: 10; /* 分成十個步驟,也就是一個步驟 60 秒 = 1 分鐘 */
}
.m::before {content: counter(count-m); }
@-webkit-keyframes oxxo-m {
to {--m: 10;}
}
/* 秒數十位數 */
.s10 {
counter-reset: count-s10 var(--s10);
--a-name: oxxo-s10;
--a-time: 60s; /* 動畫持續時間為 60 秒 */
--a-steps: 6; /* 分成六個步驟,也就是一個步驟 10 秒 */
}
.s10::before {content: counter(count-s10);}
@-webkit-keyframes oxxo-s10 {
to {--s10: 6;}
}
/* 秒數個位數 */
.s {
counter-reset: count-s var(--s);
--a-name: oxxo-s;
--a-time: 10s; /* 動畫持續時間為 10 秒 */
--a-steps: 10; /* 分成十個步驟,也就是一個步驟 1 秒 */
}
.s::before {content: counter(count-s);}
@-webkit-keyframes oxxo-s {
to {--s: 10;}
}
/* 豪秒數十位數 */
.ms10 {
counter-reset: count-ms10 var(--ms10);
--a-name: oxxo-ms10;
--a-time: 1s; /* 動畫持續時間為 1 秒 */
--a-steps: 10; /* 分成十個步驟,也就是一個步驟 0.1 秒 */
}
.ms10::before {content: counter(count-ms10);}
@-webkit-keyframes oxxo-ms10 {
to {--ms10: 10;}
}
/* 豪秒數個位數 */
.ms {
counter-reset: count-ms var(--ms);
--a-name: oxxo-ms;
--a-time: 0.1s; /* 動畫持續時間為 0.1 秒 */
--a-steps: 10; /* 分成十個步驟,也就是一個步驟 0.01 秒 */
}
.ms::before {content: counter(count-ms); }
@-webkit-keyframes oxxo-ms {
to {--ms: 10;}
}
input {display:none;}
label {cursor: pointer;}
#start:checked ~ .start {
display: none; /* 點擊開始 label ( 勾選 start 之後 ) 隱藏 start 的 label */
}
#start:checked ~ * {
--state: running; /* 點擊開始 label ( 勾選 start 之後 ) 播放動畫 */
}
.pause {display: none;} /* 暫停的 label 預設隱藏 */
#start:checked ~ .pause {
display: inline; /* 點擊開始 label ( 勾選 start 之後 ),顯示暫停 label */
}
#start:checked ~ .pause::after {
content: " ( 暫停 )"; /* 點擊開始 label ( 勾選 start 之後 ),讓暫停 label 顯示狀態 */
}
#start:checked ~ #pause:checked ~ * {
--state: paused; /* 點擊開始 label ( 勾選 start 之後 ) 同時點擊暫停 labe ( 勾選 pause ),讓動畫暫停 */
}
#start:checked ~ #pause:checked ~ .pause::after {
content: " ( 繼續 )" /* 點擊開始 label ( 勾選 start 之後 ) 同時點擊暫停 labe ( 勾選 pause ),,讓暫停 label 顯示狀態 */
}
</style>
美化程序码以及码表样式
稍微修改范例程序码,将原本单调的画面,改成类似码表的画面,就会更有码表计时的氛围,修改时使用了虚拟类别和绝对定位等相关技巧,详细说明可以参考范例注解。
<!-- HTML 程式碼 -->
<form id="stopwatch">
<div class="circle"></div>
<button id="reset" type="reset"></button>
<br>
<input type="checkbox" id="start">
<label for="start" class="start"></label>
<input type="checkbox" id="pause">
<label for="pause" class="pause"></label>
<div class="result">
<span class="num m10"></span><span class="num m"></span>:<span class="num s10"></span><span class="num s"></span>:<span class="num ms10"></span><span class="num ms"></span>
<h5>oxxo.studio</h5>
</div>
</form>
<!-- CSS 程式碼 -->
<style>
/* 自訂屬性,分鐘十位數 */
@property --m10 {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
/* 自訂屬性,分鐘個位數 */
@property --m {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
/* 自訂屬性,秒數十位數 */
@property --s10 {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
/* 自訂屬性,秒數個位數 */
@property --s {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
/* 自訂屬性,豪秒數十位數 */
@property --ms10 {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
/* 自訂屬性,豪秒數個位數 */
@property --ms {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
#stopwatch {
--state: paused;
font-family: monospace; /* 等寬字體 */
}
/* 把動畫樣式集中到一個類別中,透過變數就能產生不同的動畫效果
透過虛擬類別判斷按下開始按 checkbox 時,才會進行動畫,否則動畫會停止
--a-name 變數表示動畫名稱,--a-time 變數表示動畫時間,--a-steps 變數表示動畫步驟 */
#start:checked ~ .result .num {
animation: var(--a-name) var(--a-time) steps(var(--a-steps)) var(--state) infinite;
}
/* 分鐘數十位數 */
.m10 {
counter-reset: count-m10 var(--m10);
--a-name: oxxo-m10;
--a-time: 3600s; /* 動畫持續時間為 3600 秒 */
--a-steps: 6; /* 分成六個步驟,也就是一個步驟 600 秒 = 10 分鐘 */
}
.m10::before {content: counter(count-m10);}
@-webkit-keyframes oxxo-m10 {
to {--m10: 6;}
}
/* 分鐘數個位數 */
.m {
counter-reset: count-m var(--m);
--a-name: oxxo-m;
--a-time: 600s; /* 動畫持續時間為 600 秒 */
--a-steps: 10; /* 分成十個步驟,也就是一個步驟 60 秒 = 1 分鐘 */
}
.m::before {content: counter(count-m); }
@-webkit-keyframes oxxo-m {
to {--m: 10;}
}
/* 秒數十位數 */
.s10 {
counter-reset: count-s10 var(--s10);
--a-name: oxxo-s10;
--a-time: 60s; /* 動畫持續時間為 60 秒 */
--a-steps: 6; /* 分成六個步驟,也就是一個步驟 10 秒 */
}
.s10::before {content: counter(count-s10);}
@-webkit-keyframes oxxo-s10 {
to {--s10: 6;}
}
/* 秒數個位數 */
.s {
counter-reset: count-s var(--s);
--a-name: oxxo-s;
--a-time: 10s; /* 動畫持續時間為 10 秒 */
--a-steps: 10; /* 分成十個步驟,也就是一個步驟 1 秒 */
}
.s::before {content: counter(count-s);}
@-webkit-keyframes oxxo-s {
to {--s: 10;}
}
/* 豪秒數十位數 */
.ms10 {
counter-reset: count-ms10 var(--ms10);
--a-name: oxxo-ms10;
--a-time: 1s; /* 動畫持續時間為 1 秒 */
--a-steps: 10; /* 分成十個步驟,也就是一個步驟 0.1 秒 */
}
.ms10::before {content: counter(count-ms10);}
@-webkit-keyframes oxxo-ms10 {
to {--ms10: 10;}
}
/* 豪秒數個位數 */
.ms {
counter-reset: count-ms var(--ms);
--a-name: oxxo-ms;
--a-time: 0.1s; /* 動畫持續時間為 0.1 秒 */
--a-steps: 10; /* 分成十個步驟,也就是一個步驟 0.01 秒 */
}
.ms::before {content: counter(count-ms); }
@-webkit-keyframes oxxo-ms {
to {--ms: 10;}
}
input {display:none;}
label {cursor: pointer;}
#start:checked ~ .start {display: none;}
#start:checked ~ * {
--state: running;
}
.pause {
display: none;
}
#start:checked ~ .pause {
display: inline;
background: #0c0; /* 開始計時的時候,暫停按鈕會變成綠色 */
}
#start:checked ~ #pause:checked ~ .pause {
background: #f55; /* 暫停計時的時候,暫停按鈕會變成紅色 */
}
#start:checked ~ #pause:checked ~ * {
--state: paused;
}
/* 設定碼表表單 form 的尺寸 */
#stopwatch {
margin: 50px;
position: relative;
width: 400px;
height: 400px;
}
/* 設定碼圓形碼表的形狀,不能將表單改成圓形,因為按鈕在表單之下會無法點擊,必須額外設定另一個形狀 */
.circle {
display: block;
position: relative;
width: 400px;
height: 400px;
border-radius: 50%;
background: #ccc;
border: 3px solid #000;
z-index: 2;
}
/* 三個按鈕的位置和尺寸 */
#reset, .start, .pause {
position: absolute;
border: 3px solid #000;
width: 80px;
height: 50px;
left: calc(50% - 40px);
top: -30px;
cursor: pointer;
background: #999;
}
/* 按下 reset 時的按鈕往下移動 */
#reset:active {
top: -20px;
}
/* 數字欄位 */
.result {
position: absolute;
z-index: 2;
font-size: 60px;
background: #333;
color: #fff;
text-align: center;
padding: 10px 20px;
border-radius: 10px;
width: 300px;
left: calc(50% - 170px);
top: 150px;
pointer-events: none;
}
/* 小小說明文字 */
.result h5 {
width: 100%;
position: absolute;
font-size: 14px;
left: 0;
}
/* 暫停按鈕移動到旁邊 */
.start, .pause {
transform: rotateZ(45deg);
left: 74%;
top: 6%;
}
/* 按下暫停按鈕時,按鈕往內移動 */
.start:active, .pause:active {
left: 72%;
top: 8%;
}
/* 開始按鈕是黃色 */
.start {
background: #f90;
}
</style>
小结
CSS 码表使用了非常多 CSS 的进阶技巧,例如变数、控制动画、定位、虚拟类别...等,这些技巧虽然都很基本,但搭配起来却能产生非常酷炫的效果,不过由于 CSS 的码表“不是真的进位”,而是使用六组各自独立的数字,因此如果要制作“真正准确”的码表,建议还是使用 JavaScript 会比较妥当。
微信扫码关注
抖音扫码关注