Styled System | 從 Primer 看 GitHub 如何建構 design system

Styled Component + Styled System = GitHub/Primer

https://primer.style/components/

前言

為什麼會知道 Styled System 這個套件,是因為 GitHub 的 designer director — Diana Mounter 在 React Conf AU 2020 演講的題目「Themeability is the path to dark mode」認識了這個工具。Diana 在演講中提到 GitHub 建構了一套 design system 名為 Primer,Primer 主要是由 Styled Components 與 Styled System 所構成。聽到這邊,你應該有點興趣,GitHub 是怎麼用這兩個套件建構 Primer 的。

Primer

GitHub 以 React 為基礎,並搭配 Styled Components 與 Styled System 建構了 Primer 這套 design system,而且重要的是它是以 open source 的方式 release 在 GitHub 上,大家都可以看到 Primer 的核心程式碼。

而這套系統包含了 GitHub 網站大部分的 component,像是大家不陌生的 git 狀態圖,就可以在 Primer 中看到。

實際上,Primer 不像是 Material-UIAnt Design,適合廣泛被運用在大多數的網頁應用上,反之,Primer 專注在建構 GitHub 所用的 component,一般人平常應該不會使用到這套 design system。

但時,我們仍然可以來看看 Primer 的核心,如果有一天我們需要自己建構一套 design system,也許可以借鏡其中的知識,站在巨人的肩膀上前進。

簡介 Styled System

Styled system 是比 styled components 更低階的套件,能夠使用在 styled components 或是 emotion,並基於自定義的 theme,以及搭配 styled system,讓你能夠輕鬆建立高可擴充性樣式的 component。

styled system 的範例程式

大家先別嚇到了,看到左邊成的程式碼可能會覺得自己要改成多 CSS 的程式,但實際上這只是一種選擇性的寫法,你還是可以使用大家熟知的那套 styled component 的寫法。

const Box = styled.div`
${typography};
${space};
${color};
`;

styled system vs. tailwind

如果你使用過 tailwind,會知道 tailwind 提供許多的 utility class,像是 mt-4pb-8 等等的 space 樣式;或是更近一步的提供各種 RWD 的設定,像是 md:mx-4xl:pl-2 等等,可以減少使用 media query 定義各個螢幕尺寸下的屬性。

會提到 tailwind 的原因是因為 styled system 也包含像是 tailwind 的功能,styled system 已經內建了許多的 API,讓我們可以在定義 styled components 時,不用再特別針對一些樣式撰寫 css,而是以 props 的形式傳入 (如上圖所示)。同樣地,styled system 也可以透過傳入 array props,表示 RWD 中各個螢幕尺寸下的屬性。

筆者認為在 utility class 方面,兩者是很接近的,tailwind 使用 class,而 styled system 使用 props,就看個人或團隊更偏好哪一種撰寫方式。

實際上 styled system 還有更多的價值,像是 variant API,這個 API 可以定義 component 的多種樣式,例如 <Button /> 也許會有 success、primary、warning 等樣式,我們可以用物件的方式定義每種樣式下的 css 屬性,讓 styled component 的可讀性與可維護性更高。

styled system 沒有 ThemePrivder

前面提到 styled system 是比 styled components 與 emotion 更低階的套件,也因此 styled system 並不是自己有另一套提供 theme 的方式,而是基於 styled components 或 emotion 的 ThemeProvider 建構 theme。

Primer 的設計理念

Pattern Component vs. Helper Component

GitHub 將 Primer — component 分成兩種:

  1. Pattern Components: 有功能的 component,像是 ButtonAvatarLabel
  2. Helper Components: 輔助性的 component,像是 BoxFlexTextPosition

Primer 使用的套件

👉 polished : styled component 的 utility 套件,可以讓我們在 styled component 中使用 rgba 、 lighten 等等的 function 建立樣式:

  • rgba({ red: 255, green: 205, blue: 100, alpha: 0.7 })
  • rgba('#ffffff', 0.4)
  • lighten('0.2', 'rgba(204,205,100,0.7)')

👉 @primer/primitives : GitHub 已經定義好的一些樣式,像是 typography 、 colors ,他們在 Primer 中用了很多 @primer/primitives 所定義的樣式。

theme.js 的制定

接下來就讓我們來看 GitHub 如何使用 styled system 建立 design system,以及 Primer 的 theme.js 是如何制定。

以下是 Primer 的 theme-preval.js 的連結,包含所有 Primer 使用的 global theme。

General style vs. component style

從 theme-preval.js 中可以看到 Primer 的 global theme 被分成了 general style 與 component style 兩種:

  • General style: 指的是可以用在各個 component 的樣式,像是 color、border、box-shadow。
  • Component style: 當 component 有數種狀態時,呈現不同的樣式,像是 Button 可能會有 success、warning 、danger 等等的樣式,而 Primer 使用 styled-system 的 variant API 以物件的方式定義 component 的各種樣式

以下是 Primer 中從 theme-preval.js 所有被 export 的 global style。

General style

接下來我們來看 Primer 是如何定義 general style 的詳細內容。

👉 color

colors 的定義分為兩種:

  1. 一般的顏色:像是 yellow 、 blue、 purple 等等的顏色,這些顏色大部分都是 array,取自 @primer/primitives — colors.ts
  2. 使用在元素上的顏色:像是使用在 border 、 background 、 labels 、 text 等元素上的顏色。此外,colors 也包含自定義的元素的顏色定義,例如 可以看到 colors 中有定義 counter component 的顏色。
https://github.com/primer/components/blob/main/src/theme-preval.js#L9-L95

👉 breakpoints

在製作 RWD 網站時,大家應該不陌生 breakpoints 的設定,以下便是 Primer 的 breakpoints 設定。

不過目前不曉得 Primer 是如何制定 breakpoints,1012px 很謎 🕶,很少看到 使用 1012px 作為 large size 的邊界值。

https://github.com/primer/components/blob/main/src/theme-preval.js#L97

👉 fonts

GitHub 定義了一般字體 normal 與使用在程式碼的字體 mono

底下的 fontStack 傳入的是一個 array,簡單來說,就是把傳入的 array 組合成在 styled components 中可以讀得懂的字串格式,例如 "-apple-system, BlinkMacSystemFont, 'Segoe UI'"

https://github.com/primer/components/blob/main/src/theme-preval.js#L99-L111

👉 fontWeights

font weight 的制定就不多說,如下 👀:

https://github.com/primer/components/blob/main/src/theme-preval.js#L113-L118

👉 borderWidths

border width 的制定就不多說,如下 👀:

https://github.com/primer/components/blob/main/src/theme-preval.js#L120

👉 radii

radii 是使用在 border-radius 上的弧度,更準確來說 radii 是圓形的半徑 (r),而在 css 中的繪製方匡的圓角是利用圓形的來定義的,所以 Primer 使用 radii 作為定義 border-radius 的 global style。

https://developer.mozilla.org/zh-CN/docs/Web/CSS/border-radius
https://github.com/primer/components/blob/main/src/theme-preval.js#L122

👉 shadows

shadow 的部分由於顏色比較複雜,Primer 沒有從已經定義的 color 取得顏色的色票,而是使用 rgba 定義新的顏色。

https://github.com/primer/components/blob/main/src/theme-preval.js#L124-L134

👉 sizes

sizes 這個屬性很像 breakpoints,不一樣的地方在於 breakpoints 是 array,而 sizes 是 object,但是在 Primer 中完全沒有用到這個 sizes ,我猜應該是一個 lagacy 的 global theme 😅。

https://github.com/primer/components/blob/main/src/theme-preval.js#L136-L141

👉 fontSizes

font size 的制定就不多說,如下 👀:

https://github.com/primer/components/blob/main/src/theme-preval.js#L143

👉 space

sapce 大部分被使用在 margin 與 padding 的樣式,有少數很謎的 🕶 出現在 top 上面。

https://github.com/primer/components/blob/main/src/theme-preval.js#L145

Component style

👉 buttons

Primer 的 buttons 共有四種樣式,分別為以下四種。

https://github.com/primer/components/blob/main/src/theme-preval.js#L149-L248

每一種樣式都有四個 key,分別是 colorborderbgshadow ,在每個 key 裡面,又分別定義了 button 在各種狀態時顯示的樣式,像是 button :

  • 在 default 狀態時的 color 是 colors.text.grayDark
  • 在 disabled 狀態時的 color 是 gray[4]

原本我以為在 theme-preval.js 中定義的 component style 會被用在 variant API 上,但實際上並非如此。

https://github.com/primer/components/blob/main/src/theme-preval.js#L149-L248

從下圖中可以看到 Primer 中不同樣式的 button 被包裝成不同的 component,並不是使用 styled system 的 variant API 來指定樣式。

https://primer.style/components/Buttons

雖然從官方文件中可以看到 <Button /> 仍然有 variant props,但是它是被用來設定 component 的 font size。如果想要直接設定 font size,可以傳入 fontSize props,將會覆蓋 variant🔗 設定

https://primer.style/components/Buttons#button

接著,我們看到 <ButtonDanger /> 程式碼,<ButtonDanger /> 直接使用了 theme-preval.js 中定義的 theme,並沒有用 variant 來指定 button 的 color、border、background-color、box-shadow 等等樣式。

❗❗由此可知,我們了解了在 theme-preval.js 中 button的樣式定義是被用來歸納 button 會用到的各種樣式,至於 variant API 則是用來獨立設定 font size。

如果想知道 Button 的 variant font size 是如何定義,可以看 🔗這個連結

https://github.com/primer/components/blob/main/src/Button/ButtonDanger.js

👉 flash

<Flash /> 看起來像是一個 Box,很像是 bootstrap 提供的 .alert。而在 theme 中定義了background-colorbroder-color

https://github.com/primer/components/blob/main/src/theme-preval.js#L250-L267

你可以在下圖中範例可以看到,當我們想要 default、success、warning、danger 其中一種樣式時,可以傳入 variant props,便可以得到相對應樣式的 component。

https://primer.style/components/Flash
https://primer.style/components/Flash#component-props

<Flash />🔗程式碼 中可以看到,variant 中加上 scale: 'flash' ,表示從 theme-preval.js 中拿到 flash 的樣式,然後再透過 variant props 就能夠快速設定相對應的樣式。

❗❗由此可知,在 theme-preval.js 中定義 <Flash /> 的樣式,確實是用在 variant API 上,與 <Button /> 的使用方式不同。

你可以思考看看究竟 theme.js 中定義的內容是要拿來定義 component 在各種狀態下的 variant 樣式,還是像 button 的方式直接封裝成不同的 component。

https://github.com/primer/components/blob/main/src/Flash.js

👉 flashIcon

FlashIcon 主要被用在 <Flash /> 上的 icon,因為 <Flash /> 中的 icon 跟背景顏色不一樣,為了做出區隔,Primer 將它們分成兩個 theme 的變數。

https://github.com/primer/components/blob/main/src/theme-preval.js#L270-L275
https://primer.style/components/Flash#with-an-icon

🔗程式碼 中可以看到,在 <Flash /> 傳入的 variant props 將會帶入到 svg 的顏色,再透過 getIconColor 可以從 theme-preval.js 中拿到定義的樣式。

❗❗由此可知,在 theme-preval.js 中定義的 flashIcon,是透過 <Flash />variant props 間接設定 icon 的顏色,也算是間接使用了 variant API 的功能。

https://github.com/primer/components/blob/main/src/Flash.js

👉 popovers

Popover是一個 Tooltip 功能的 component,點擊 button 後會跳出一個提示小方框。

在 theme-preval.js 中的 popovers 僅僅只有一個樣式的定義,與前面看到的component 擁有多種樣式不太一樣

https://github.com/primer/components/blob/main/src/theme-preval.js#L277-L281
https://primer.style/components/Popover
https://primer.style/components/Popover#popover

<Popover /> 的 props 定義來看,它看起來僅僅只是一個普通的 component,但如果從 🔗程式碼 來看,其實可以傳入的 props 很多,其中 9~10 行是 styled system 提供的功能,讓 component 可以透過 props 定義:

  • color
  • spacing (mtpx 等)
  • layout (widthheightdisplay 等)
  • position (relative) 。

❗❗由此可知,所見不一定是即所得,你甚至可以拿到得更多,Primer 因為使用的 styled system 的緣故,我們可以使用不少沒有定義在文件上的 props。

要不要制定 design system 時,如果你使用的是 styled system,需要思考要不要暴露出 styled system 的 props。有很多 Primer 的 component 都有暴露出來,但是沒有寫在文件上,也或者是團隊的共識,才能這樣使用 styled system。

https://github.com/primer/components/blob/main/src/Popover.js

👉 pagination

pagination 定義的 theme 很多樣,包括設定 border-radius、space、color 等。

https://github.com/primer/components/blob/main/src/theme-preval.js#L283-L309

但是從使用 <Pagination /> 的文件中可以看到該 component 沒有設定樣式的 props,而是傳入功能性的 props,上述在 theme-preval.js 中定義的 colors 是直接在 🔗程式碼 中被引用。

https://primer.style/components/Pagination

❗❗ 由此可知,<Pagination /> 的一些樣式是在 theme-preval.js 中管理,但是同樣地,文件中沒有寫道可以使用 styled system 的 API,可是你能夠自在 🔗程式碼 中看到些蛛絲馬跡 (propType),說明了 Pagination 這個 component 能夠在外部傳入一些由 styled system 提供改變樣式的 props。

https://primer.style/components/Pagination#component-props

👉 StateLabels

在 GitHub 中 git 的狀態訊息,像是 open、closed、merged 之類的提醒,分成 size 與 status 兩種:

  • size 是這個狀態的大小。
  • status 是各種 git 狀態的背景顏色。
https://github.com/primer/components/blob/main/src/theme-preval.js#L311-L343

<StateLabel /> 的樣式是靠傳入 status props 指定該 lable 的顏色,另外從文件中可以看到還有另一個 variant props 可以傳入,用來指定 label 的 size。

https://primer.style/components/StateLabel

❗❗我們可以從 <StateLabel /> 的程式碼中看到, variantstatus 這兩個 props 都是透過 variant API 設定,讓外面的 component 可以決定 <StateLabel /> 的樣式。

https://github.com/primer/components/blob/main/src/StateLabel.js

結論

我們現在大致上了解了 GitHub 是如何透過 styled system 建立 Primer 這套 design system,僅僅只看有透過 theme-preval.js 決定 component 樣式的僅僅只有六個,包括 button、flash、flashIcon、popovers、pagination 與 stateLable。

button、flash、flashIcon 與 stateLabel 這四個 component 使用了 variant API 從外部決定 component 樣式。但是 button 是一個例外,該 component 傳入的 variant props 其屬性是在 ButtonBase.js 中定義,並非事先定義於 theme-preval.js 之中。

pagination 與 popovers 比較像是普通的功能性 component,只是把一些樣式定義於 theme-preval.js 裡面,統一管理 component 的主題樣式。

從 Primer 的程式碼看到的問題

目前大致上看過 Primer 的程式碼後會覺得 GitHub 比較沒有系統性的建立 design system,如果你有到 Primer 的官方網站上看看 Primer 提供的 component ,你會發現有很多 component 的 theme 並沒有定義在 theme-preval.js 之中,而是定義 variant 在 component 的檔案裡面。

其實挺亂的 😅,團隊需要有共識哪些要放到 theme.js 中,或者是直接定義在 component 裡面。

再者,如果團隊想要使用像是 styled system 這種提供使用 props 來定義樣式的套件,需要達成定義 component 的規範,像是 Primer 的文件雖然沒有說明每個 component 都可以傳入改變樣式的 props,但實際上團隊有個不成文規定是所有的 component 都可以這樣做,也許未來團隊需要思考這一點。

團隊究竟要不要 theme 的功能?

最近經常在思考這個問題,團隊如果加入 theme 的功能,勢必要花費一些成本在構築 theme 的系統上,包括設計師參與的成本、溝通成本、開發複雜度增加的成本,以及如果使用到 styled system,程式碼將會高度耦合 styled system 的使用方式,未來有一天想改動高耦合的程式碼,痛苦程度肯定不低。

畢竟 styled system 看上去不是非常成熟,目前只有看到 Primer 這個比較大的套件在使用而已。

我認為在 theme 需要能夠讓使用者「自由替換主題」時才能展現出它的價值,例如常見的 dark mode 與 light mode 的切換,否則過早的導入整套的 theme,也許也不見得是件好事。

分享就到這邊,如果喜歡我的文章可以幫我拍個幾下手,在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。 😃

--

--

一群技術人想要寫出一些好文章所建立的技術專欄。每週二一篇原創文章、一封電子報,歡迎大家訂閱!主網站: https://weekly.starbugs.dev/。

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Airwaves

Airwaves

每天進步一點點,在終點遇見更好的自己。