如何讓CSS計數器支持小數的動態變化?

CSS 計數器是個好東西
原理其實很簡單,content?雖然本身不支持 CSS 變量直接渲染,但是可以支持counter-reset。
count::before {
--percent: 50;
counter-reset: progress var(--percent);
content: counter(progress);
}
通過一次中轉,就可以讓content也能支持CSS變量作為字符展示了。

這個技巧是通過張鑫旭的這篇文章了解的,非常實用:小tips: 如何借助content屬性顯示CSS var變量值 [1]。
但是,這個方法有個比較遺憾的地方就是,CSS 計數器不支持真正意義上的小數,也就是如果 CSS 變量為小數的話,直接展示為 0。
count::before {
--percent: 50.15;
counter-reset: progress var(--percent);
content: counter(progress);
}

那么,如何讓content也支持CSS變量的小數展示呢,畢竟很多情況下還是需要小數的?比如下面這個,如果支持了小數,就可以輕易地實現帶小數的數字滾動動畫。

今天一起來探討一下。
一、CSS 原理拆解
CSS 計數器由于特殊性,目前都是僅支持整數的,畢竟自然個數是沒有小數的(不排除以后自定義計數器可以實現)。既然這樣,可以換一種思路,從數字形態上進行拆分。比如一個小數,48.69?可以分解成整數部分48?和小數部分69,然后再通過小數點鏈接起來。這樣拆分后就都是整數了, CSS 計數器也是支持的。

用代碼實現就是(便于理解,以下的一些變量都是中文命名的,實際生產不推薦)。
count::before {
--整數: 48;
--小數: 69;
counter-reset: 整數計數器 var(--整數) 小數計數器 var(--小數);
content: counter(整數計數器) "." counter(小數計數器);
}

所以問題就變成了,如何將一個小數進行拆分呢?
二、CSS變量拆分成整數和小數
接著上面的問題,假設變量是--percent?,問題就是下面兩個變量--整數和--小數?如何通過--percent計算而來呢?
count::before {
--percent: 48.69;
--整數: 48;
--小數: 69;
counter-reset: 整數計數器 var(--整數) 小數計數器 var(--小數);
content: counter(整數計數器) "." counter(小數計數器);
}
看似很容易,但在 CSS 中好像并不怎么好實現。
為了解決這個,需要了解一下 CSS 自定義變量的類型[2]。類型有很多,下面羅列一下。
- <length>
- <number>
- <percentage>
- <length-percentage>
- <color>
- <image>
- <url>
- <integer>
- <angle>
- <time>
- <resolution>
- <transform-function>
- <custom-ident>
- <transform-list>
大部分能可以看出具體的類型,我們這里需要用到的就兩種,<number>?和<integer>,兩者都表示數字,具體的區別在于
- <number>表示任意的數字,整數和小數都可以。
- <integer>表示整型數字,只能是整數,小數會認為不合法。
回到這里,默認情況下,CSS 變量可以是任意值,但是通過自定義變量@property可以指定變量的類型,它可以對不合法的變量進行轉換。
@property - CSS(層疊樣式表) | MDN (mozilla.org)[3]。
比如,我們需要一個整數,可以這樣來定義,將syntax?屬性設置為<integer>就可以了。
@property --整數 {
syntax: "<integer>"; /*整型*/
initial-value: 0;
inherits: false;
}
這樣,這個變量會被強制轉換成整數。比如,下面給--整數也設置成一個小數。
count::before {
--percent: 48.69;
--整數: 48.69;
--小數: 69;
counter-reset: 整數 var(--整數) 小數 var(--小數);
content: counter(整數) "." counter(小數);
}
結果...

居然直接變成了 0?
不過沒關系,需要可以配合一些 CSS 計算函數實現自動轉換,比如calc。
count::before {
--percent: 48.69;
--整數: calc(48.69);/*使用 CSS 計算后可以轉換成整數*/
--小數: 69;
counter-reset: 整數 var(--整數) 小數 var(--小數);
content: counter(整數) "." counter(小數);
}

但是,這里變成了49?,原因其實是四舍五入造成的,并不是向下取整。為了消除這種誤差,可以再減去0.5,所以整數部分的最終實現就是。
@property --整數 {
syntax: "<integer>";
initial-value: 0;
inherits: false;
}
count::before {
--percent: 48.69;
--整數: calc(var(--percent) - 0.5);
--小數: 69;
counter-reset: 整數 var(--整數) 小數 var(--小數);
content: counter(整數) "." counter(小數);
}
未來的 CSS 數學函數應該也會有 floor、ceil 這樣的,可以期待一下~
然后是小數部分,有了整數部分,小數部分就容易了,可以用整個值減去整數部分,然后乘以 100,示意如下。

用代碼實現就是。
@property --小數 {
syntax: "<integer>";
initial-value: 0;
inherits: false;
}
count::before {
--percent: 48.69;
--整數: calc(var(--percent) - 0.5);
--小數: calc((var(--percent) - var(--整數)) * 100 - 0.5);
counter-reset: 整數 var(--整數) 小數 var(--小數);
content: counter(整數) "." counter(小數);
}
效果如下:

后面最末位的小數由于四舍五入的關系稍微有些偏差,沒關系,可以修正一下,加上??0.01??就行了。其次,還有一個問題,當小數位小于 10 的時候,計算出的結果可能是這樣。

那么,這種情況就需要動態補零了。
所以,只需要在計數器后面定義一下計數器樣式decimal-leading-zero,表示十進制前置零,最終實現如下:
count::before {
--percent: 48.69;
--整數: calc(var(--percent) - 0.5);
--小數: calc((var(--percent) - var(--整數)) * 100 - 0.5 + 0.01);
counter-reset: 整數 var(--整數) 小數 var(--小數);
content: counter(整數) "." counter(小數, decimal-leading-zero);
}
這樣整數和小數都可以用同一個變量--percent表示出來了,完美~
三、CSS 變量動畫
有人可能會覺得,為啥要廢這么大勁去實現這樣一個功能?用 js 直接設置不行嗎?如果僅僅是數字的變化,那當然可以,但在這里,除了CSS 單一變量帶來更好的可維護性外, 還可以做到連 JS 也難以做到(或者說成本更高)的事情,比如過渡動畫
首先,再改進一下,很多小數都是百分比形式的,也就是0~1?范圍內,所以前面--percent?可能是這樣的值0.4869。
count::before {
--percent: 0.4869;
--百分比: calc(var(--percent) * 100);
--整數: calc(var(--百分比) - 0.5);
--小數: calc((var(--百分比) - var(--整數)) * 100 - 0.5 + 0.01);
counter-reset: 整數 var(--整數) 小數 var(--小數);
content: counter(整數) "." counter(小數, decimal-leading-zero) "%";
}
效果如下:

然后,我們通過 JS 讓這個數字隨機變化。
count.addEventListener('click', ev => {
ev.target.style.setProperty("--percent", Math.random());
})
效果如下:

但是,這樣太死板了,我們需要數字變化的時候有個動畫,可以直接通過 CSS 自定義變量實現。
@property --percent {
syntax: "<number>";
initial-value: 0;
inherits: false;
}
count{
/**/
transition: --percent 1s
}
現在看看效果,非常輕松的就實現了數字的滾動動畫。

小數部分由于是跟隨整數部分的,比如整數從1?變為3,那么小數部分就跟隨變化兩個循環。本來這個也非常符合常理,就像時鐘的秒永遠要比分要轉的快一樣,但是有人可能覺得變的太快了,有沒有辦法讓小數部分和整數部分獨立開來呢?當然也是可以的,而且非常容易,只需要給整數部分和小數部分分別設置過渡就行了。
count{
/**/
transition: --整數 1s, --小數 1s;
}
現在再看看效果,和上面對比一下。

這兩種效果可以自行選擇,僅僅只是過渡的不同。
試想一下,如果這個效果用 JS 來實現,是不是還有點點麻煩呢?
下面是完整代碼(不多,就這么幾行)。
@property --percent {
syntax: "<number>";
initial-value: 0;
inherits: false;
}
@property --整數 {
syntax: "<integer>";
initial-value: 0;
inherits: false;
}
@property --小數 {
syntax: "<integer>";
initial-value: 0;
inherits: false;
}
count {
--percent: 0.4512;
font-size: 60px;
font-weight: bolder;
cursor: pointer;
font-family: 'Courier New', Courier, monospace;
--百分比: calc(var(--percent) * 100);
--整數: calc(var(--百分比) - 0.5);
--小數: calc((var(--百分比) - var(--整數)) * 100 - 0.5 + 0.01);
counter-reset: 整數 var(--整數) 小數 var(--小數);
transition: --整數 1s, --小數 1s;
}
count::before {
content: counter(整數) "." counter(小數, decimal-leading-zero) "%";
}
你也可以訪問線上 demo:CSS double num(codepen.io)[4] 或者 CSS double num(runjs.work)[5] 或者 CSS double num (juejin.cn)[6]。
四、總結和說明
以上就是全部內容了,一個還不錯的小技巧,你學會了嗎?
- CSS 變量不支持直接在content中渲染,但是可以借助計數器初始化來實現。
- CSS 計數器不支持小數初始化。
- CSS 計數器支持小數的實現原理在于將小數拆分為整數、小數點、小數三個部分。
- CSS 自定義變量可以指定變量的類型,這樣通過 CSS 數學函數可以將一個小數轉換成整數。
- 小數部分可以通過減去整數部分得到。
- 小數部分還需要通過decimal-leading-zero補全位數。
- CSS 單一變量一方面可以帶來更好的可維護性,另一方面還可以更輕易地實現過渡動畫。
- 借助@property可以很方便的控制 CSS 變量的過渡和動畫。
數字變化動畫在一些數據大屏展示的場景下還是挺實用的,有了 CSS 變量,再也不需要通過 JS 去實時計算了。不過目前兼容性還不是太好,適合內部項目小范圍使用(當然直接用了不要緊,不支持的只是沒有動畫而已)。
























