是時候開始使用 CSS 自定義屬性了
已發表: 2022-03-10我們使用預處理器來存儲顏色、字體偏好、佈局細節——主要是我們在 CSS 中使用的所有內容。
自定義元素的詳細介紹
您可能聽說過 Web 組件以及它們將如何永遠改變 Web 開發。 最具變革性的技術是自定義元素,這是一種定義您自己的元素的方法,具有自己的行為和屬性。 閱讀簡介 →
但是預處理器變量有一些限制:
- 您不能動態更改它們。
- 他們不知道 DOM 的結構。
- 它們無法從 JavaScript 讀取或更改。
作為解決這些問題和其他問題的靈丹妙藥,社區發明了 CSS 自定義屬性。 從本質上講,它們的外觀和工作方式類似於 CSS 變量,它們的工作方式反映在它們的名稱中。
自定義屬性為 Web 開發開闢了新的視野。
聲明和使用自定義屬性的語法
當您開始使用新的預處理器或框架時,通常的問題是您必須學習新的語法。
每個預處理器都需要不同的聲明變量的方式。 通常,它以保留符號開頭——例如,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{ --spacer: 10; } .box{ padding: var(--spacer)px 0; /* DOESN'T work */ padding: calc(var(--spacer)*1px) 0; /* WORKS */ }
範圍和繼承
在討論 CSS 自定義屬性範圍之前,讓我們回顧一下 JavaScript 和預處理器範圍,以便更好地理解它們的區別。
我們知道,例如 JavaScript 變量 ( var
),作用域僅限於函數。
let
和const
也有類似的情況,但它們是塊範圍的局部變量。
JavaScript 中的closure
是一個可以訪問外部(封閉)函數變量的函數——作用域鏈。 閉包具有三個作用域鏈,它可以訪問以下內容:
- 它自己的範圍(即在其大括號之間定義的變量),
- 外部函數的變量,
- 全局變量。
預處理器的情況類似。 讓我們以 Sass 為例,因為它可能是當今最流行的預處理器。
使用 Sass,我們有兩種類型的變量:局部變量和全局變量。
全局變量可以在任何選擇器或構造之外聲明(例如,作為 mixin)。 否則,變量將是本地的。
任何嵌套的代碼塊都可以訪問封閉變量(如在 JavaScript 中)。
這意味著,在 Sass 中,變量的作用域完全取決於代碼的結構。
但是,CSS 自定義屬性是默認繼承的,並且與其他 CSS 屬性一樣,它們是級聯的。
你也不能有一個在選擇器之外聲明自定義屬性的全局變量——這不是有效的 CSS。 CSS 自定義屬性的全局範圍實際上是:root
範圍,因此該屬性是全局可用的。
讓我們利用我們的語法知識,將 Sass 示例改編為 HTML 和 CSS。 我們將使用原生 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 }
請參閱 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 3。
但是在 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; }
請參閱 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 2。
這是第一個巨大的區別:如果您重新分配自定義屬性的值,瀏覽器將重新計算應用它的所有變量和calc()
表達式。
預處理器不知道 DOM 的結構
假設我們想要使用塊的默認font-size
,除了highlighted
的類存在的地方。
這是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)); }
因為具有default
類的第二個 HTML 元素帶有highlighted
類,所以highlighted
類的屬性將應用於該元素。
在這種情況下,它意味著–highlighted-size: 30px;
將被應用,這反過來將使被分配的font-size
屬性使用–highlighted-size
。
一切都很簡單並且有效:
請參閱 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 4。
現在,讓我們嘗試使用 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; } }
結果顯示默認大小適用於兩者:
請參閱 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 5。
發生這種情況是因為所有 Sass 計算和處理都發生在編譯時,當然,它對 DOM 的結構一無所知,完全依賴於代碼的結構。
如您所見,自定義屬性具有變量作用域的優點,並添加了 CSS 屬性的通常級聯,了解 DOM 的結構並遵循與其他 CSS 屬性相同的規則。
第二個要點是 CSS 自定義屬性知道 DOM 的結構並且是動態的。
CSS-Wide 關鍵字和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 變量的名稱是“自定義屬性”,那麼為什麼不使用它們來模擬不存在的屬性呢?
其中有很多: translateX/Y/Z
, background-repeat-x/y
(仍然不兼容跨瀏覽器), box-shadow-color
。
讓我們嘗試使最後一個工作。 在我們的示例中,讓我們在懸停時更改 box-shadow 的顏色。 我們只想遵循 DRY 規則(不要重複自己),所以我們不會在:hover
部分重複box-shadow
的整個值,而是改變它的顏色。 救援的自定義屬性:
.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; */ }
請參閱 CodePen 上 Serg Hospodarets (@malyw) 使用 CSS 自定義屬性的 Pen Emulating "box-shadow-color" CSS 屬性。
顏色主題
自定義屬性最常見的用例之一是應用程序中的顏色主題。 創建自定義屬性就是為了解決這類問題。 因此,讓我們為組件提供一個簡單的顏色主題(應用程序可以遵循相同的步驟)。
這是我們的按鈕組件的代碼:
.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
HTML 類(例如, body
元素)並在應用時更改顏色:
body.inverted .btn{ --shadow-color: #888888; --gradient-from-color: #CB6724; --gradient-to-color: #D67F46; --color: #000000; }
下面是一個演示,您可以在其中單擊一個按鈕來添加和刪除一個全局類:
請參閱 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 9。
如果沒有復制代碼的開銷,則無法在 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 = 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 = getComputedStyle(breakpointsData) .getPropertyValue('--phone');
為了展示如何從 JavaScript 分配自定義屬性,我創建了一個響應用戶操作的交互式 3D CSS 立方體演示。
這不是很難。 我們只需要添加一個簡單的背景,然後放置五個立方體面,其中包含transform
屬性的相關值: translateZ()
、 translateY()
、 rotateX( 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); };
現在,當用戶移動鼠標時,演示會更改視圖。 您可以通過移動鼠標並使用鼠標滾輪放大和縮小來檢查這一點:
請參閱 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 10。
本質上,我們只是更改了 CSS 自定義屬性的值。 其他一切(旋轉和放大和縮小)都由 CSS 完成。
提示:調試 CSS 自定義屬性值的最簡單方法之一就是在 CSS 生成的內容中顯示其內容(這適用於簡單的情況,例如字符串),以便瀏覽器自動顯示當前應用的值:
body:after { content: '--screen-category : 'var(--screen-category); }
您可以在純 CSS 演示(沒有 HTML 或 JavaScript)中檢查它。 (調整窗口大小以查看瀏覽器自動反映更改後的 CSS 自定義屬性值。)
瀏覽器支持
所有主流瀏覽器都支持 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 生態系統今天提供了許多插件。 假設您僅提供全局變量(即您僅在:root
選擇器中聲明或更改 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,一個可以在 Github 上找到的 Sass 混合。 使用它,您可以開始使用 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 自定義屬性並準備在瀏覽器中支持它們的正確時機。