純 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 會比較妥當。
意見回饋
如果有任何建議或問題,可傳送「意見表單」給我,謝謝~