iT邦幫忙

2025 iThome 鐵人賽

1
Modern Web

關於那些有趣的 CSS 效果系列 第 35

( Day 35 ) 純 CSS 碼表 ( 可計時 )

  • 分享至 

  • xImage
  •  

在小時候那個沒有手機的年代,最喜歡電子表的「碼表」功能,三不五時就會跟同學玩起「最接近一秒」的遊戲 ( 現在應該還是有人在玩 ),為了實現童年的回憶,我用純 CSS 手刻了一個碼表,但時光荏苒歲月如梭,當年的高雄金城武已經變成金正恩,高雄劉德華也變成了劉的華,也沒有人可以一起玩碼表了...( 想太多 )

純 CSS 碼表 ( 可計時 )

正文開始

這篇教學會使用非常多的 CSS 進階技巧,包含 CSS 變數、動畫控制、結構選擇器、虛擬類別選擇器、自訂屬性、清單計數器...等,透過這些技巧的互相搭配組合,就能在不使用 JavaScript 的情況下,透過純 CSS 製作出可以計時的碼表。

CSS 教學 - 純 CSS 碼表 ( 可計時 )

製作可以暫停和繼續的數字

使用「純 CSS」製作「可以暫停和繼續」的數字需要用到許多進階技巧,暫停和繼續的控制方式是依靠「CSS 變數」和「動畫暫停與繼續」,數字紀錄的方式依靠「自訂屬性」和「計數器」,最後透過虛擬元素顯示數值,詳細說明可參考下方範例程式碼註解。

html >>>>
<!-- 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>

CSS 教學 - 純 CSS 碼表 ( 可計時 ) - 製作可以暫停和繼續的數字

延伸上述程式碼,將其改成使用 inputcheckbox 元素,並將負責暫停和繼續的變數移動到 checked 狀態的選擇器裡,就能做到點擊開始,再點一下就暫停的效果。

html >>>>
<!-- 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>

CSS 教學 - 純 CSS 碼表 ( 可計時 ) - 點擊後可以暫停和繼續的數字

加入分鐘數、秒數和毫秒數

延伸上述程式碼,將其改成使用 inputcheckbox 元素,並將負責暫停和繼續的變數移動到 checked 狀態的選擇器裡,就能做到點擊開始,再點一下就暫停的效果。

html >>>>
<!-- 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 教學 - 純 CSS 碼表 ( 可計時 ) - 加入分鐘數、秒數和毫秒數

加入重新啟動機制

如果要單純使用 CSS 產生重新啟動的機制,必須要稍微修改 HTML 程式碼,修改重點如下:

  • 使用 form 元素包覆 buttoncheckbox 元素。
  • 透過 buttonreset 類型重置 checkbox 的狀態。
  • 新增負責「開始」的 checkbox
  • 修改原本「暫停/繼續」的 checkbox,和開始的 checkbox 互相搭配。
  • 按下重置 button 時,停止 CSS 動畫
  • 按下開始 checkbox 時,啟動 CSS 動畫
  • 按下暫停 checkbox 時,暫停 CSS 動畫

下方範例除了按照上述重點修改程式碼,也額外讓開始與暫停 checkbox「只顯示其中一種」,如此就能透過 activeckecked 的虛擬類別,判斷目前按鈕狀態,進而決定動畫的播放狀態。

線上展示:https://codepen.io/oxxo/pen/azbxqOP

html >>>>
<!-- 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>

CSS 教學 - 純 CSS 碼表 ( 可計時 ) - 加入重新啟動機制

美化程式碼以及碼表樣式

稍微修改範例程式碼,將原本單調的畫面,改成類似碼表的畫面,就會更有碼表計時的氛圍,修改時使用了虛擬類別和絕對定位等相關技巧,詳細說明可以參考範例註解。

html >>>>
<!-- 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 碼表使用了非常多 CSS 的進階技巧,例如變數、控制動畫、定位、虛擬類別...等,這些技巧雖然都很基本,但搭配起來卻能產生非常酷炫的效果,不過由於 CSS 的碼表「不是真的進位」,而是使用六組各自獨立的數字,因此如果要製作「真正準確」的碼表,建議還是使用 JavaScript 會比較妥當。


上一篇
( Day 34 ) 純 CSS 數字進度條 ( 會動呦 )
系列文
關於那些有趣的 CSS 效果35
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言