用 var() 取值
CSS 中的 var()
函數是一個非常實用的工具,用來讀取自定義屬性 (也就是 CSS 變數) 的值。其語法如下:
var( <custom-property-name>, <fallback-value>? )
基本規則
-
第一個參數必須為 CSS 變數:直接填入數值(例如
var(20px)
)會導致錯誤,因為var()
僅接受自定義屬性名稱。 -
var()
不能用來取代屬性名稱:換句話說,我們無法使用var(--prop-name): 20px;
這樣的寫法,因為var()
僅限於屬性值部分。
.foo {
margin: var(20px); /* 錯誤,20px 不是 CSS 變數 */
--prop-name: margin-top;
var(--prop-name): 20px; /* 錯誤,不能這樣寫 */
}
細部行為
-
var(--b, fallback_value)
的第二個參數作為預設值:當--b
未定義或是無效時,將使用fallback_value
。 -
var(--c,)
語法無誤,空的預設值:當--c
無效時,預設值會是空白,這裡逗號的存在表示我們有提供預設值,即使是空的。 -
var(--d, var(--e), var(--f), var(--g))
呢?依據規範的話其實第一個逗號,
之後的都是預設值,也就是當--d
是 invalid 的時候會計算var(--e), var(--f), var(--g)
來作結果的值 -
var()
是一個完整的 CSS token:例如20px
是一個完整的 token,無法由var()
分開組合,var(--size)var(--unit)
不會生成20px
。 -
initial
與 CSS 變數:直接給 CSS 變數賦值initial
,表示該變數值為初始無效值,若想讓屬性顯示initial
,則需要將initial
放在預設值內。 -
URL 與 var() 的特性:
url()
被視為一個完整的 CSS token,若要使用 URL,必須在變數定義時包含url()
。
:root {
/* 1. */
margin: var(--b, 20px); /* 若 --b 無效,則 margin 使用 20px */
/* 2. */
padding: var(--c,) 20px; /* --c 無效時,padding 為 20px */
/* 3. */
font-family: var(--fonts, "lucida grande", tahoma, Arial); /* 若 --fonts 無效,則使用 "lucida grande", tahoma, Arial */
/* 4. */
--text-size: 12;
--text-unit: px;
font-size: var(--text-size)var(--text-unit); /* 錯誤,解析為 12 和 px 而非 12px */
/* 5. */
--initialized: initial;
background: var(--initialized, initial); /* 結果為 background: initial */
/* 6. */
--invalid-url: "https://useme.medium.com";
background: url(var(--invalid-url)); /* 無效,因為 url() 內的 var() 無法解析 */
--valid-url: url(https://useme.medium.com);
background: var(--valid-url); /* 正確使用方式 */
}
變數的解析
CSS 變數其實也是一種 CSS 屬性,遵循 CSS 權重規則。深入了解權重如何影響 CSS 變數的應用,有助於更精確地控制樣式的範圍。
-
全域變數與區域變數:在
:root
定義的變數適用於整個文件,若放在不同選擇器的話則會有相對於選擇器的作用範圍。:root { --main-color: blue; /* 全域適用 */ } .container { --main-color: green; /* 只在 .container 生效 */ }
-
權重的優先順序:權重高的變數定義會覆蓋權重低的變數。
:root { --main-color: blue; } .section { --main-color: green; /* 覆蓋 :root 定義 */ } .section p { color: var(--main-color); /* 顯示綠色 */ } p { color: var(--main-color); /* 顯示藍色 */ }
<div class="section"> <p>這段文字會使用 .section 的 --main-color,為綠色。</p> </div> <p>這段文字會使用 :root 的 --main-color,為藍色。</p>
-
變數定義依權重計算順序:像 CSS 屬性一樣,變數值的計算依權重由小至大。
:root { --red: 255; --green: 255; --blue: 255; --background: rgb(var(--red), var(--green), var(--blue)); } .box { --green: 0; background: var(--background); }
在這個範例中,
.box
的背景顏色是白色,並不會因為.box
設定了--green: 0
就變成 rgb(255, 0, 255)。這是因為--background
在計算值的時候是以當下看到的var(--red), var(--green), var(--blue)
來決定,而-green: 0
是出現在.box
的定義內,此時已經無法影響前面決定的值了。 -
同層級的 pseudo-classes 重新計算變數:這類變數會隨伺 pseudo-classes 的狀態而變更。
:root { --red: 255; --green: 255; --blue: 255; } .box { --background: rgb(var(--red), var(--green), var(--blue)); background: var(--background); } .box:hover { --green: 0; /* hover 時背景顏色改變 */ }
接下來,看一下兩個特別的 CSS 變數用法:
進階應用 A:動畫
CSS 變數無法直接與動畫搭配,原因是瀏覽器無法推測變數的資料型態。為解決這個問題,可搭配 @property
定義變數的型態與初始值,讓瀏覽器理解該變數如何應用於動畫。
@property --green {
syntax: "<number>";
initial-value: 255;
inherits: false;
}
.section {
padding: 5em;
background: rgb(50, var(--green), 50);
transition: --green 0.5s;
}
.section:hover {
--green: 50;
}
HTML
<div class="section">
<p>這段底色使用 CSS 變數 + 動畫</p>
</div>
這段程式碼中,我們透過 @property
將 --green
定義為 <number>
型態,設定初始值為 255。當滑鼠移到 .section
上時,--green
變為 50,實現動態顏色切換的效果。
進階應用 B:淺色/深色模式切換
若想做主題色的切換,就把顏色抽出變成變數,依據 prefers-color-scheme
即可在系統的淺色與深色模式之間切換。
:root {
--background-color: #FBFBFB;
--container-background-color: #EBEBEB;
--headline-color: #0077EE;
--text-color: #333333;
}
@media (prefers-color-scheme: dark) {
:root {
--background-color: #121212;
--container-background-color: #555555;
--headline-color: #94B2E6;
--text-color: #e0e0e0;
}
}
加入手動切換同時整合系統設定
以上是依據系統設定決定主題色系,但若想讓使用者自行控制該怎麼作?
這時候就要多一個 checkbox 來承載切換狀態。直覺上 checkbox 選取時是深色模式、沒有選取時是淺色模式,這樣我們可以利用 CSS 變數加上 :has()
很輕鬆的處理變數的切換。
我想試著用純 CSS 完成這件事情,但使用者的系統設定有可能是淺色或深色,而 CSS 無法做到在深色模式的時候把 checkbox 改為選取狀態。
山不轉路轉,我們需要稍微繞一下,變成這樣:
- 用 CSS 實作一個 toggle,視覺上 OFF 代表淺色模式、ON 代表深色模式
-
系統設定淺色模式:checkbox 沒有選取時,對應到 OFF 的狀態,代表淺色模式。而選取時,對應到 ON 的狀態,代表使用深色模式
-
系統設定深色模式:因為系統預設的設定剛好相反,因此呈現也需要反過來,也就是說 checkbox 沒有選取時,對應到 ON 的狀態,代表使用深色模式。選取時,對應到 OFF 的狀態,使用淺色模式。
要達到這樣的效果,有兩個要點:
第一,變數依據系統設定以及 checkbox 狀態決定其值
:root {
--background-color: #FBFBFB;
--container-background-color: #EBEBEB;
--headline-color: #0077EE;
--text-color: #333333;
}
:root:has(input[type="checkbox"]:checked) {
--background-color: #121212;
--container-background-color: #555555;
--headline-color: #94B2E6;
--text-color: #e0e0e0;
}
@media (prefers-color-scheme: dark) {
:root {
--background-color: #121212;
--container-background-color: #555555;
--headline-color: #94B2E6;
--text-color: #e0e0e0;
}
:root:has(input[type="checkbox"]:checked) {
--background-color: #FBFBFB;
--container-background-color: #EBEBEB;
--headline-color: #0077EE;
--text-color: #333333;
}
}
第二,則是 toggle 依據系統設定決定 checked 狀態與 ON / OFF 的關聯性
可以看到深色跟淺色模式時候的 CSS 屬性剛好是相反的
/* Toggle Switch Styles */
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
/* Hide the checkbox element to style a custom switch */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* Slider styling for the switch background */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--slider-bg, #ccc);
transition: 0.4s;
border-radius: 34px;
}
/* Slider knob styling */
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: 0.4s;
border-radius: 50%;
}
/* Dark mode styles: make the switch look "checked" by default */
@media (prefers-color-scheme: dark) {
.slider {
background-color: #94b2e6;
}
.slider:before {
transform: translateX(26px); /* Move knob to the right */
}
/* Invert checked state in dark mode to look "unchecked" */
input:checked + .slider {
background-color: #ccc;
}
input:checked + .slider:before {
transform: translateX(0); /* Move knob to the left */
}
}
/* Light mode styles: make the switch look "unchecked" by default */
@media (prefers-color-scheme: light) {
.slider {
background-color: #ccc;
}
.slider:before {
transform: translateX(0); /* Knob on the left */
}
/* Invert checked state in light mode to look "checked" */
input:checked + .slider {
background-color: #94b2e6;
}
input:checked + .slider:before {
transform: translateX(26px); /* Move knob to the right */
}
}
簡化變數設定
第一點的變數設定我們可以用 CSS 變數的神奇技巧 Space Toggle 再進行一些簡化,直接先上 code 再來介紹其機制:
:root {
--ON: initial; /* Default state variable to use for switching colors */
--OFF: ; /* Alternative state variable for switching colors */
/* Set default color variables based on light mode */
--light: var(--ON);
--dark: var(--OFF);
/* Define custom properties for colors used in light and dark modes */
--background-color: var(--light, #fbfbfb) var(--dark, #121212);
--container-background-color: var(--light, #ebebeb) var(--dark, #555555);
--headline-color: var(--light, #0077ee) var(--dark, #94b2e6);
--text-color: var(--light, #333333) var(--dark, #e0e0e0);
}
:root:has(input[type="checkbox"]:checked) {
--light: var(--OFF);
--dark: var(--ON);
}
這個技巧的重點在 --background-color: var(--light, #fbfbfb) var(--dark, #121212);
這一行,這個背景顏色則是依據 --light
跟 --dark
的值來決定的,有點像是我們把 if / else 直接寫在屬性內。
如何決定?可以看到初始情況下 --light: var(--ON)
,而 --ON: initial
代表了 --ON
一開始是一個 invalid 的狀態(請見前一篇文),而 --OFF
則是空白。這個套用到 var(--light, #fbfbfb) var(--dark, #121212)
的時候,因為第一個 var()
的 --light
是 invalid 所以會取用預設值 #fbfbfb
,第二個 var()
的 --dark
則是有效變數,空白,因此 -background-color
會等於 #fbfbfb
加上空白。
下面其他變數都是一樣的邏輯,依據 --light
跟 --dark
兩個變數的狀態來決定最後使用的是深色還是淺色的主題,這個設定只需要每一個顏色變數設定一次。
而後續切換狀態就簡單了,當下如果是深色模式時,就是 --light: var(--OFF);
加上 --dark: var(--ON);
。而如果是淺色模式,就是相反的--light: var(--OFF);
加上 --dark: var(--ON);
。 這裡邏輯有不是很直覺,不過以目前 CSS 的功能來說似乎只能這樣兜出這種判斷器,如果有更好的作法也請告知。
最後完成的範例: https://codepen.io/finfin/pen/VwoqgKx
總結
CSS 功能持續的在擴增,2016 年後 CSS 變數就已經出現在主流瀏覽器上,後續的 @property
, :has()
都讓 CSS 變數可以有更彈性的使用方法。甚至新的 scroll-driven animation 也有許多搭配 CSS 變數來呈現的神奇技巧,之後有機會再來介紹。