在今天,css預加載已經成為了前端開發的一個標準。預加載的一個主要優勢就是可以讓你使用變量。它可以讓你避免復制黏貼你的代碼,并且簡化了開發和重構。
我們用預加載來存儲顏色、字體偏好,布局的細節——幾乎是我們在css里面用到的所有東西。
但是預加載的變量有一些使用上的限制。
- 你不能動態的改變他們。
- 他們不能認出DOM結構
- 他們不能用JavaScript讀取和改變。
為了解決這樣或那樣的問題,社區發明了CSS自定義屬性。本質上這看上去和實現起來就像CSS變量。并且他們的實現方式就像他們的名字一樣。
自定義屬性為前端開發打開了新的大門。
深入閱讀
申明和使用自定義屬性的語法
在你開始學習心得預處理器或者框架的使用通常的問題是你必須學習新的語法。
每一種預處理器都使用不同的方法申明變量。通常使用一個保留字符作為開始——舉個例子,Sass的$
、LESS的@
。
CSS自定義屬性同樣也是用保留字符 --
來引入聲明。但是好處是你只需要學一遍語法就能在所有瀏覽器上使用。
你可能會問,“為什么不使用已經有的語法?”
這是有原因的。簡單來說這提供了一種在任何預處理器中使用自定義屬性的方式。在這種方式下我們可以使用自定義屬性,預處理器也不會編譯他們,所以這些屬性會直接輸出到編譯后的CSS中。并且你也可以重復使用預處理器變量在源文件中,這個我稍后會細說。
(關于這個名字:因為他們的想法和目標非常相似,有些時候自定義屬性被叫做CSS變量,盡管正確名稱叫CSS自定義屬性,往下讀你就會明白為什么這個名字是最正確的。)
所以要聲明一個變量來代替常用的CSS屬性,就像color
或者padding
,用--
連接一個自定義名稱屬性就可以:
.box{
--box-color: #4d4e53;
--box-padding: 0 10px;
}
屬性的值可以是任何有效的CSS值:顏色、字符串、布局甚至是表達式。
這里是有效的自定義屬性的例子:
:root{
--main-color: #4d4e53;
--main-bg: rgb(255, 255, 255);
--logo-border-color: rebeccapurple;
--header-height: 68px;
--content-padding: 10px 20px;
--base-line-height: 1.428571429;
--transition-duration: .35s;
--external-link: "external link";
--margin-top: calc(2vh + 20px);
/* Valid CSS custom properties can be reused later in, say, JavaScript. */
--foo: if(x > 5) this.width = 10;
}
以防萬一你不知道什么是:root
匹配,在HTML里他就等同與html
標簽,但是具有更高的特異性。
自定義屬性和其他的CSS屬性一樣是動態的、級聯的。這意味著他們能在任何時候被改變是由瀏覽器來進行的。
為了使用自定義的變量,你需要使用var()
CSS函數,并且提供屬性參數。
.box{
--box-color:#4d4e53;
--box-padding: 0 10px;
padding: var(--box-padding);
}
.box div{
color: var(--box-color);
}
聲明和用例
var()
函數有一個非常便利的提供默認值的方法。如果你不確信自定義屬性已經被定義并且需要一個默認值,函數的第二個參數用來作為默認值的。
.box{
--box-color:#4d4e53;
--box-padding: 0 10px;
/* 10px is used because --box-margin is not defined. */
margin: var(--box-margin, 10px);
}
你可能會希望在聲明新的變量的時候能重復使用已有的變量值:
.box{
/* The --main-padding variable is used if --box-padding is not defined. */
padding: var(--box-padding, var(--main-padding));
--box-text: 'This is my box';
/* Equal to --box-highlight-text:'This is my box with highlight'; */
--box-highlight-text: var(--box-text)' with highlight';
}
運算:+,-,*,/
既然我們習慣使用預處理器和其他語言,我們也希望在處理變量的時候能使用基本運算。為了達到這個目的,CSS提供了calc()
函數,作用當自定義屬性的值被改變的時候瀏覽器會重新計算表達式:
:root{
--indent-size: 10px;
--indent-xl: calc(2*var(--indent-size));
--indent-l: calc(var(--indent-size) + 2px);
--indent-s: calc(var(--indent-size) - 2px);
--indent-xs: calc(var(--indent-size)/2);
}
特別是當你想用一個沒有單位的值得時候,就需要使用calc()
函數
:root{
--gap: 10;
}
.box{
padding: var(--spacer)px 0; /* DOESN'T work */
padding: calc(var(--spacer)*1px) 0; /* WORKS */
}
作用域和繼承
在討論CSS自定義屬性的作用域之前,我們先來回顧一下JavaScript和預處理器的作用域。這樣就能更好的認識他們之間的區別。
我們知道在JavaScript中如果在函數中使用var
關鍵字聲明變量,那么他的作用域就在函數里面。
同樣的我們可以使用let
和const
關鍵字,但他們的作用域相對于變量的塊作用域(We have a similar situation with let
and const
, but they are block-scope local variables.)
在JavaScript中閉包(closure)是一個可以訪問外部函數變量的函數——作用域鏈。閉包有三個作用域鏈:
- 它自己的作用域(即 變量定義在大括號中)
- 外部函數的變量
- 全局變量

預處理器也是相同的,讓我們用Sass來舉個例子。因為這大概是今天最流行的預處理器了。
在Sass中有兩種類型的變量:當前作用域變量(local) 和 全局變量。
一個全局變量能被申明在選擇器和構造器(比如mixin)外,其他的變量就是當前作用域變量

這意味著在Sass中變量的作用域完全依賴代碼的嵌套結構。
然而CSS自定義屬性完全和其他的CSS屬性一樣使用級聯方式默認繼承。
當然你也不能在CSS中定義一個在選擇器外的屬于全局的自定義屬性變量,因為這不是有效的CSS。自定義屬性的全局作用域實際上就是:root
的作用域,這里面定義的屬性就是全局的。
讓我們用已知的語法知識和Sass的例子來創建一個使用原生的CSS自定義屬性的例子,首先是HTML:
global
<div class="enclosing">
enclosing
<div class="closure">
closure
</div>
</div>
CSS:
:root {
--globalVar: 10px;
}
.enclosing {
--enclosingVar: 20px;
}
.enclosing .closure {
--closureVar: 30px;
font-size: calc(var(--closureVar) + var(--enclosingVar) + var(--globalVar));
/* 60px for now */
}
到目前為止我們還沒有看出他和Sass變量有什么區別。然而讓我們重新給變量賦值:
在Sass中,是無效的。
.closure {
$closureVar: 30px; // local variable
font-size: $closureVar +$enclosingVar+ $globalVar;
// 60px, $closureVar: 30px is used
$closureVar: 50px; // local variable
}
但是在CSS中計算的值改變了。因為font-size
的值因為--closureVar
值得改變重新計算了。
.enclosing .closure {
--closureVar: 30px;
font-size: calc(var(--closureVar) + var(--enclosingVar) + var(--globalVar));
/* 80px for now, --closureVar: 50px is used */
--closureVar: 50px;
}
這是第一個非常大的區別:如果你對自定義屬性重新賦值,瀏覽器會重新計算所有的變量和calc()
表達式。【會不會有性能問題?】
預處理器不能識別DOM結構
假如我們想在除了class
是highlighted
的div
上使用默認的font-size
HTML:
<div class="default">
default
</div>
<div class="default highlighted">
default highlighted
</div>
CSS:
.highlighted {
--highlighted-size: 30px;
}
.default {
--default-size: 10px;
/* Use default-size, except when highlighted-size is provided. */
font-size: var(--highlighted-size, var(--default-size));
}
因為第二個div
元素使用了highlighted
類,在highlighted
類上的屬性就提供給這個元素了。
在這里就意味著,--hightlighted-size: 30px
被提供了。是的font-size的屬性被重新賦值了。
一切都是這么直截了當的運行
接下來讓我們嘗試使用Sass來實現同樣的例子
.highlighted {
$highlighted-size: 30px;
}
.default {
$default-size: 10px;
/* Use default-size, except when highlighted-size is provided. */
@if variable-exists(highlighted-size) {
font-size: $highlighted-size;
}
@else {
font-size: $default-size;
}
}
結果顯示他們都使用默認字體大小
這是因為所有的Sass的計算和進程都發生在編譯過程中,所以理所當然的他不知道DOM結構,所以依賴代碼結構。
如你所見自定義屬性在變量作用域和通常的css級聯樣式上具有優勢。并且能夠識別DOM結構。和普通的CSS屬性使用相同的語法規則。
第二個例外是CSS自定義屬性能動態的識別DOM結構
CSS關鍵字和all
屬性
CSS自定義屬性遵守與常規CSS自定義屬性相同的規則。這意味著您可以為其分配任何常見的CSS關鍵字:
-
inherit
此CSS關鍵字應用元素的父對象的值。
-
initial
這將應用CSS規范中定義的初始值(空值,或在某些CSS自定義屬性的情況下)。
-
unset
在自定義屬性中,如果屬性是繼承的,則應用繼承的值,如果屬性是初始化的值,則引用初始化值
-
revert
這會將該屬性重置為用戶代理樣式表建立的默認值(在CSS自定義屬性的情況下為空值)。
.common-values{
--border: inherit;
--bgcolor: initial;
--padding: unset;
--animation: revert;
}
我們來看另外一個例子。假設你想構建一個組件,并且想要確保沒有其他樣式或自定義屬性被無意中應用(在這種情況下,通常會使用模塊化的CSS解決方案)。
現在還有另一種方法:使用all
CSS屬性。這個簡寫將重置所有CSS屬性。
與CSS關鍵字一起,我們可以執行以下操作:
.my-wonderful-clean-component{
all: initial;
}
這會為我們的組件重置所有的樣式
不幸的是,all關鍵字不會重置自定義屬性。關于是否添加 - 前綴,這將重置所有CSS自定義屬性,正在進行討論。
所以在將來,一個完整的重置會是這樣的:
.my-wonderful-clean-component{
--: initial; /* reset all CSS custom properties */
all: initial; /* reset all other CSS styles */
}
CSS自定義屬性用例
有許多自定義屬性使用的方式,在這里我會展示他們中最有趣的部分。
模擬不存在的CSS規則
這些CSS變量的名稱是自定義屬性,那為什么我們不能用它來模擬不存在的CSS屬性?
有很多比如translateX/Y/Z
,background-repeat-x/y
(仍然不能跨瀏覽器兼容),box-shadow-color
讓我們試著模擬最后一個屬性。在這個例子里當hover
的時候我們改變box-shadow
的顏色。我們只想遵循DRY規則(不要重復你自己),所以我們只是改變它的顏色,而不是在:hover部分重復box-shadow的整個值。(變量的改變會重新計算var()
和calc()
)
.test {
--box-shadow-color: yellow;
box-shadow: 0 0 30px var(--box-shadow-color);
}
.test:hover {
--box-shadow-color: orange;
/* Instead of: box-shadow: 0 0 30px orange; */
}
顏色主題
自定義屬性有一個最常用的用例就是應用程序中的顏色主題。自定義屬性就是用來解決這類問題的。所以,讓我們為一個組件提供一個簡單的顏色主題(應用程序可以遵循相同的步驟)。
這是button組件的代碼
.btn {
background-image: linear-gradient(to bottom, #3498db, #2980b9);
text-shadow: 1px 1px 3px #777;
box-shadow: 0px 1px 3px #777;
border-radius: 28px;
color: #ffffff;
padding: 10px 20px 10px 20px;
}
我們假設要反轉顏色主題。
第一步是將所有顏色變量擴展到CSS自定義屬性并重寫我們的組件。重寫后的代碼
.btn {
--shadow-color: #777;
--gradient-from-color: #3498db;
--gradient-to-color: #2980b9;
--color: #ffffff;
background-image: linear-gradient(
to bottom,
var(--gradient-from-color),
var(--gradient-to-color)
);
text-shadow: 1px 1px 3px var(--shadow-color);
box-shadow: 0px 1px 3px var(--shadow-color);
border-radius: 28px;
color: var(--color);
padding: 10px 20px 10px 20px;
}
這有我們需要的一切。使用它,我們可以將顏色變量重寫為反轉值,并在需要時應用它們。例如,我們可以添加全局inverted
類(例如,body元素),并在應用顏色時更改顏色:
body.inverted .btn{
--shadow-color: #888888;
--gradient-from-color: #CB6724;
--gradient-to-color: #D67F46;
--color: #000000;
}
以下是一個演示,您可以在其中單擊一個按鈕來添加和刪除全局類:demo
如果不重復代碼,在CSS預處理器中無法實現此行為。使用預處理器,您將始終需要覆蓋實際的值和規則,這往往會導致額外的CSS。
使用CSS自定義屬性,解決方案可以盡可能的干凈,復制黏貼是可以避免的。因為只需要對變量進行重新賦值。
在JavaScript中使用自定義屬性
以前,要將數據從CSS發送到JavaScript,我們經常不得不采取技巧,通過CSS輸出中的純JSON編寫CSS值,然后從JavaScript讀取它.
現在,我們可以輕松地使用JavaScript中的CSS變量進行交互,使用眾所周知的.getPropertyValue()
和.setProperty()
方法讀取和寫入它們,這些方法用于通常的CSS屬性:
/**
* Gives a CSS custom property value applied at the element
* element {Element}
* varName {String} without '--'
*
* For example:
* readCssVar(document.querySelector('.box'), 'color');
*/
function readCssVar(element, varName){
const elementStyles = window.getComputedStyle(element);
return elementStyles.getPropertyValue(`--${varName}`).trim();
}
/**
* Writes a CSS custom property value at the element
* element {Element}
* varName {String} without '--'
*
* For example:
* readCssVar(document.querySelector('.box'), 'color', 'white');
*/
function writeCssVar(element, varName, value){
return element.style.setProperty(`--${varName}`, value);
}
假設我們有一系列的媒體查詢值
.breakpoints-data {
--phone: 480px;
--tablet: 800px;
}
因為我們只想在JavaScript中重用它們 - 例如,在Window.matchMedia()中,我們可以輕松地從CSS中獲取它們
const breakpointsData = document.querySelector('.breakpoints-data');
// GET
const phoneBreakpoint = window.getComputedStyle(breakpointsData)
.getPropertyValue('--phone');
為了展示如何從JavaScript分配自定義屬性,我創建了一個交互式3D CSS 立方體demo,以響應用戶操作。
這不是很難我們只需要添加一個簡單的背景,然后放置五個立方體面與transform屬性的相關值:translateZ(),translateY(),rotateX()和rotateY()。
#world{
--translateZ:0;
--rotateX:65;
--rotateY:0;
transform-style:preserve-3d;
transform:
translateZ(calc(var(--translateZ) * 1px))
rotateX(calc(var(--rotateX) * 1deg))
rotateY(calc(var(--rotateY) * 1deg));
}
唯一缺少的是交互性。當鼠標移動時,演示應該更改X和Y視角(--rotateX和-rotateY),當鼠標滾動(--translateZ)時應該放大和縮小)。
這是JavaScript的訣竅:
// Events
onMouseMove(e) {
this.worldXAngle = (.5 - (e.clientY / window.innerHeight)) * 180;
this.worldYAngle = -(.5 - (e.clientX / window.innerWidth)) * 180;
this.updateView();
};
onMouseWheel(e) {
/*…*/
this.worldZ += delta * 5;
this.updateView();
};
// JavaScript -> CSS
updateView() {
this.worldEl.style.setProperty('--translateZ', this.worldZ);
this.worldEl.style.setProperty('--rotateX', this.worldXAngle);
this.worldEl.style.setProperty('--rotateY', this.worldYAngle);
};
現在,當用戶移動鼠標時,演示會更改視圖。您可以通過移動鼠標并使用鼠標滾輪放大和縮小來檢查:demo
基本上,我們只是更改了CSS自定義屬性的值。其他(旋轉和放大和縮小)都是由CSS完成的。
提示:調整CSS自定義屬性值的最簡單方法之一就是在CSS生成的內容中顯示其內容(在簡單的情況下,例如使用字符串),以便瀏覽器將自動顯示當前應用的值:
body:after {
content: '--screen-category : 'var(--screen-category);
}
您可以在純CSS演示(無HTML或JavaScript)中查看。 (調整窗口大小,查看瀏覽器會自動反映更改后的CSS自定義屬性值。)
:root {
--screen-category: 'desktop';
}
@media screen and (max-width: 1024px) {
:root {
--screen-category: 'tablet';
}
}
@media screen and (max-width: 640px) {
:root {
--screen-category: 'phone';
}
}
body:after {
content: '--screen-category : 'var(--screen-category);
}
瀏覽器支持
主流瀏覽器都支持了CSS自定義屬性

這意味著你可以自己開始使用它們。
如果您需要支持舊版瀏覽器,您可以學習語法和使用示例,并考慮并行切換或使用CSS和預處理器變量的可能方法。
當然,我們需要能夠檢測CSS和JavaScript中的支持,以便提供回退或增強功能。
這很容易對于CSS,您可以使用帶有虛擬功能查詢的@supports條件:
@supports ( (--a: 0)) {
/* supported */
}
@supports ( not (--a: 0)) {
/* not supported */
}
在JavaScript中,您可以使用與CSS.supports()靜態方法相同的虛擬自定義屬性:
const isSupported = window.CSS &&
window.CSS.supports && window.CSS.supports('--a', 0);
if (isSupported) {
/* supported */
} else {
/* not supported */
}
我們看到,CSS自定義屬性在每個瀏覽器中仍然不可用。知道這一點,您可以通過檢查它們是否受支持來逐步增強您的應用程序。
例如,您可以生成兩個主要的CSS文件:一個具有CSS自定義屬性,另一個沒有它們,其中屬性是內聯的(我們將在稍后討論一些方法)。
默認加載第二個。然后,如果支持自定義屬性,只需檢查JavaScript并切換到增強版本即可:
<!-- HTML -->
<link href="without-css-custom-properties.css"
rel="stylesheet" type="text/css" media="all" />
// JavaScript
if(isSupported){
removeCss('without-css-custom-properties.css');
loadCss('css-custom-properties.css');
// + conditionally apply some application enhancements
// using the custom properties
}
這只是一個例子。往下看,有更好的選擇。
如何開始使用它們
針對最近的一項調查,Sass已經成為了開發社區中預處理器的最佳選擇。
所以,讓我們考慮開始使用CSS自定義屬性或使用Sass為他們做準備的方法。
我們有一些觀點
1.手動檢查代碼支持
手動檢查代碼中自定義屬性是否支持的方法的一個優點是如果它可行我們就可以直接用它(不要忘記我們已經切換到Sass):
$color: red;
:root {
--color: red;
}
.box {
@supports ( (--a: 0)) {
color: var(--color);
}
@supports ( not (--a: 0)) {
color: $color;
}
}
這種方法確實有許多缺點,其中不僅僅是代碼變得復雜,而且復制和粘貼變得很難維護。
2.使用自動轉換CSS的插件
PostCSS生態系統今天提供了幾十個插件。它們中的幾個在生成的CSS輸出中處理自定義屬性(內聯值),并使它們工作,假設您僅提供全局變量(即,您只聲明或更改:根選擇器中的CSS自定義屬性),因此它們的值可以輕松內聯。
其中一個例子就是postcss-custom-properties
這個插件提供了幾個優點:它使語法工作;它與PostCSS的所有基礎設施兼容;并且不需要太多的配置。
但是有一些缺點。該插件需要您使用CSS自定義屬性,因此您沒有準備項目以從Sass變量切換的路徑。此外,您將無法對轉換進行很多控制,因為在Sass被編譯為CSS之后完成。最后,插件不提供很多調試信息。
3. CSS-VARS MIXIN
我開始在我大多數項目里使用CSS自定義屬性并且嘗試了很多策略:
- 用cssnext從Sass切換到PostCSS
- 從Sass變量切換到純CSS自定義屬性
- 在Sass中使用CSS變量來檢測是否支持它們。
通過這些經驗,我開始尋找一個可以滿足我的標準的解決方案:
- 它應該很容配合Sass來使用
- 應該直接使用,并且語法必須盡可能接近原生的CSS自定義屬性。
- 將CSS輸出從內聯值切換到CSS變量應該很容易。
- 熟悉CSS自定義屬性的團隊成員將能夠使用該解決方案。
- 應該有一種方法有使用變量的調試信息。
因此,我創建了css-vars,一個Sass mixin,可以在Github上找到。使用它,你就可以使用CSS自定義屬性語法。
使用 css-vars Mixin
聲明變量,使用的mixin如下:
$white-color: #fff;
$base-font-size: 10px;
@include css-vars((
--main-color: #000,
--main-bg: $white-color,
--main-font-size: 1.5*$base-font-size,
--padding-top: calc(2vh + 20px)
));
使用這些變量,用var()
函數:
body {
color: var(--main-color);
background: var(--main-bg, #f00);
font-size: var(--main-font-size);
padding: var(--padding-top) 0 10px;
}
這為您提供了一種從一個地方(從Sass)控制所有CSS輸出并開始熟悉語法的方法。此外,您可以使用mixin重用Sass變量和邏輯。
當您想要支持的所有瀏覽器都使用CSS變量時,您需要做的就是添加:
$css-vars-use-native: true;
而不是調整生成的CSS中的變量屬性,mixin將開始注冊自定義屬性,并且var()
實例將轉到生成的CSS而不進行任何轉換。這意味著您將完全切換到CSS自定義屬性,并具有我們討論的所有優點。
如果你想打開有用的調試信息,如下:
$css-vars-debug-log: true;
這會給你
- 當變量沒有被定義卻被使用的日志
- 變量被重復定義的日志
- 當默認值代替了未定義變量值的日志
結束語
現在你對CSS自定義屬性有了更多的了解,包括語法、優勢、一些有用的例子還有如何在JavaScript中進行交互
你需要知道如何確認他們是否被支持,他們和CSS預處理器的變量有什么區別,以及如何在瀏覽器支持之前開始使用原生的CSS變量。
這是開始使用CSS自定義屬性并為瀏覽器支持做準備的最佳時機。