Tailwind CSS 到底是良藥還是毒藥?

過去在實作功能的時候,大致都切分成三部份:HTML、CSS 和 JavaScript,各自有各自的任務。HTML 負責版面與元件要怎麼安排、CSS 負責樣式呈現,以及 JavaScript 負責動作,而 CSS 會用 class 作為三者連接的橋樑。

舉例來說,這裡有個商品區塊。這是一個甜點店的菜單,左側放商品的名稱與價格,右邊放圖。

Tailwind CSS 到底是良藥還是毒藥? 商品區塊 過去的寫法

HTML 的部份會這樣撰寫。

<div class="product-item">
  <div class="product-header">
    <div class="product-item-title">
      <span>抹茶千層蛋糕</span>
      <span>($280)</span>
    </div>
    <img alt="抹茶千層蛋糕" src="抹茶千層蛋糕.jpeg" />
  </div>
</div>

CSS 的部份會這樣撰寫。

.product-item {
  display: flex;
  padding: 10px;
  background: #e3d5ca;
}

.product-header {
  display: flex;
  gap: 10px;
}

.product-item-title {
  display: flex;
  gap: 10px;
  color: #222222;
  align-items: center;
}

這樣看起來沒什麼問題,若今天來了個需求,要做個類似的元件,但不同處是它不是陳列圖片,而是選調色盤上的顏色,下圖表示有個選顏色為藍色的選項。

Tailwind CSS 到底是良藥還是毒藥? 選調色盤上的顏色

HTML 的部份會這樣撰寫。

<div class="color-item">
  <div class="color-header">
    <div class="color-item-title">
      <div>這是藍色方塊</div>
    </div>
    <div class="color-item-block blue"></div>
  </div>
</div>

CSS 的部份會這樣撰寫。

.color-item {
  display: flex;
  padding: 10px;
  background: #e3d5ca;
}

.color-header {
  display: flex;
  gap: 10px;
}

.color-item-title {
  display: flex;
  gap: 10px;
  color: #222222;
  align-items: center;
}

.color-item-block {
  width: 60px;
  height: 60px;
}

.color-item-block.blue {
  background: blue;
}

從這裡可以發現,class name 並沒有共用,原因是 class name 的語意不合,像是 prefix 為 product 暗示是商品相關的功能,放在表示顏色的區塊上就不適用,很可能會造成團隊其他開發者的誤解和困擾。也就是說,開發這些類似的功能,似乎要不停重複撰寫樣式?這讓我們思考,有什麼辦法在就算碰到語意不合的問題,依然可以共用呢?

再來,就算是實作同樣的功能好了,假設都是商品區塊,本來是圖片放右邊、文字放左邊,如果某天想要換成文字放右邊、圖片放左邊,那就勢必要在增加一條樣式 .image-left 來覆寫。

<div class="product-item">
  <div class="product-header image-left">
    <div class="product-item-title">
      <span>抹茶千層蛋糕</span>
      <span>($280)</span>
    </div>
    <img alt="抹茶千層蛋糕" src="https://dummyimage.com/60x60/000/fff" />
  </div>
</div>
.product-item .image-left {
  flex-direction: row-reverse;
}

今天 image 要放左邊,明天選顏色的區塊也要放左邊,那是不是大家都要有個 .image-left.color-left 呢?很多地方都有類似的需求,多一個需求就要多寫一條規則,有沒有辦法共用呢?像是用個 .left 之類的規則同時適用於商品和顏色區塊呢?先講一下當年沒有 flex 語法,我怕寫了古早的東西很多人看不懂,像是 float XD

總結來說,以上遇到的問題,大致歸納為這兩種:

維護小專案還好,如果是在做大型網站,每新增一條樣式規則就是在增加維護的難度。

樣式規則很多為什麼會增加維護的難度呢?

舉例來說,在做某個電商網站時,商品要陳列的資訊都差不多,大多是商品名稱、描述還有封面圖這些東西,商品頁有商品頁的規則,分類頁有分類頁的規則,就會遇到這些問題。像是商品區塊本來都是圖片在右邊,現在要新增圖片可以放左邊的功能,可能會改哪些地方呢?至少會有首頁、商品頁和分類頁吧?疑,好像還有購物車、結帳頁?功能點起來是不是很複雜?

假設沒有對這產品超熟,一定只會改到開發者自己知道的頁面(大概就是首頁、商品頁分類頁、購物車和結帳頁),最多就是再用相關關鍵字做全域搜尋,像是 .image-left 用到的地方,看看有沒有什麼要調整的,於是就會漏了很少用的功能像是歷史購買紀錄頁面。

很不巧的是,歷史購買紀錄頁面是這樣寫的,把 imageleft 拆開,連關鍵字搜尋 .image-left 都找不到。

const getImageAlignClassName = (align) =>
  align === 'left' ? 'left' : 'center';
const imageClassName = 'image' + getImageAlignClassName(align);

雖然這種問題可以在 code review 時提出來討論,只是在趕功能開發的狀況下,大家的時間都是有限的,很可能會忽略這樣的細節。

當年我同事參考了實現 Atomic CSS、OOCSS 的概念和 Bootstrap 作為範例,幫大家設計一套樣式規則,像是以下這樣。

.display-reverse {
  flex-direction: row-reverse;
}

.align-item-center {
  display: flex;
  align-items: center;
}

.w-60 {
  width: 60px;
}

.h-60 {
  height: 60px;
}

.p-1x {
  padding: 10px;
}

.text-dark-gray {
  color: #222222;
}

.bg-brown {
  background: #e3d5ca;
}

.bg-blue {
  background: blue;
}

其他類似的概念還有 BEM 和 SMACSS 等,這裡就先不提了,有興趣可以看這篇文章

我們可以拿這個規則,改寫以上的例子,先來修改圖片放在右邊的例子。

<div class="p-1x dispaly-flex bg-brown">
  <div class="dispaly-flex">
    <div class="dispaly-flex align-item-center text-dark-gray">
      <span>抹茶千層蛋糕</span>
      <span>($280)</span>
    </div>
    <img alt="抹茶千層蛋糕" src="https://dummyimage.com/60x60/000/fff" />
  </div>
</div>

接著修改圖片放在左邊的例子。

<div class="p-1x dispaly-flex bg-brown">
  <div class="dispaly-flex display-reverse">
    <div class="dispaly-flex align-item-center text-dark-gray">
      <span>抹茶千層蛋糕</span>
      <span>($280)</span>
    </div>
    <img alt="抹茶千層蛋糕" src="https://dummyimage.com/60x60/000/fff" />
  </div>
</div>

最後修改顏色區塊的例子。

<div class="p-1x dispaly-flex bg-brown">
  <div class="dispaly-flex">
    <div class="dispaly-flex align-item-center text-dark-gray">
      <span>這是藍色方塊</span>
    </div>
    <div class="bg-blue w-60 h-60"></div>
  </div>
</div>

附上範例程式碼與 Demo

修改後的三段程式碼感覺上長得很像,好像差不多。這是因為修改後基本上就是用那幾個 class name 重複做組合排列而已,大家一旦熟悉規則,根本不用重寫樣式,寫好 HTML 之後放上該用的 class name 就完工了。當時我們做了好幾個專案都這樣玩,開發速度變得超快,產能++。

前面提到的例子和本文想探討的 Atomic CSSTailwind CSS 稍微有點差異,畢竟是當時依照產品、專案需求和團隊狀況調整過的規則,雖然同樣想要把樣式模組化、好重用,但實際上的 Atomic CSS 更著重將樣式屬性獨立成單一 class name,且命名 class name 會與屬性與其值類似,而 Tailwind CSS 是個基於 Atomic CSS 概念開發的框架。

修改範例以 Atomic CSS 實作會是這樣寫。

<div class="D(f) P(2) Bg(#e3d5ca)">
  <div class="D(f)">
    <div class="D(f) C(#222222) Ai(c)">
      <span>抹茶千層蛋糕</span>
      <span>($280)</span>
    </div>
    <img alt="抹茶千層蛋糕" src="https://dummyimage.com/60x60/000/fff" />
  </div>
</div>

修改範例以 Tailwind CSS 實作會是這樣寫。

<div class="p-2 flex bg-stone-400">
  <div class="flex">
    <div class="flex items-center text-neutral-800">
      <span>抹茶千層蛋糕</span>
      <span>($280)</span>
    </div>
    <img alt="抹茶千層蛋糕" src="https://dummyimage.com/60x60/000/fff" />
  </div>
</div>

根據指令而非語意,這樣的設計概念就是大家所熟知的 Atomic CSS,還有現在火紅的 Tailwind CSS。

這樣的設計方式解決的什麼問題呢?

首先,我認為最重要的就是避免樣式覆蓋以及彈性高這兩個優點,這兩個優點帶來的好處真的解決了自古以來 CSS 的原罪全域污染(命名衝突 name collision)與不夠結構化(重用性 reusability)的問題。CSS 的原罪就是 CSS 只有全域而沒有區域的概念,也就是說,只要當前頁面的 DOM 結構符合載入樣式的規則,就會被套用這個樣式。

再來,關於效能

最後,關於產能

那有沒有什麼需要注意的呢?

更多疑難雜症的解答可參考官方說明

總結

坦白說近年我沒有常態地在用 Tailwind CSS,大多就是 side project 上用用而已,很多時候工作上是用公司自己開發的 UI library 來做的,而它們也提供了足夠客製化樣式的屬性。但依據過去 Atomic CSS 和這些在的 side projec 的經驗來做討論,Tailwind CSS 到底是良藥還是毒藥?在考量要不要用 Tailwind CSS 時,我會這樣認為:

最後,最近看到滿多人在討論到底要不要用 Tailwind CSS,於是想說來分享個人在實務上的經驗和看法,這些故事都是真實發生過的點滴,不過也不用特別對號入座摟。


備註

註 1:(2024/02/16 更新)近期有看到關於 StyleX 的討論,同樣我也是沒在用 StyleX 只用過 Styled Components 這樣 CSS in JS 的解法,的確 CSS in JS 解決 (1) 全域狀況下命名衝突所造成的樣式覆蓋的問題;(2) JS 與 CSS 都在同一支 JS 檔案,能共享變數來做邏輯判斷,不用間接新增或移除某個 class 來控制樣式;(3) CSS in JS 將樣式寫在元件裡面,讓重用變得簡單,因為要重用這個元件同時也重用了它的樣式,因此重構也變得相對容易;(4) 由於樣式和元件合併了,因此只會載入要用到的 CSS 程式碼,不相關的都不會載入,增進瀏覽器的載入效能。

但我認為 CSS in JS 這個解法很吃開發者實作元件的能力,像是在多個元件嵌套的狀況下,覆寫樣式很可能會變得跟原始的 CSS 寫法差不多,只是範圍從全域變成元件、變得比較小而已,這樣就沒有享受到更好的樣式管理方式與開發體驗。

舉例來說,<StyledParent> 包含 <StyledChild>

<StyledParent>
  <StyledChild className="hello">test</StyledChild>
</StyledParent>

<StyledParent> 設定樣式,當底下有元素加上 .hello 這個 class 時,字體顏色為藍色。

const StyledParent = styled.div`
  .hello {
    color: blue;
  }
`;

const StyledParent = () => {
  // 略...
};

但在 <StyledChild> 已設定樣式,希望字體顏色是紅色。

const StyledChild = styled.div`
  color: red;
`;

const StyledChild = () => {
  // 略...
};

結果是 test 的字體會是藍色,如果想追查為什麼不是紅色,就會發現沒有用到預期我們想 CSS in JS 能管控元件樣式的目的。當然這個例子主要是發生在覆寫第三方套件,以及團隊成員對於元件的掌握度或樣式撰寫風格沒有一致想法的狀況了。

用 Tailwind CSS 改寫如下,與其在 CSS in JS 觀察複雜 CSS 的結構,對我來說 Atomic CSS 把樣式埋在 class 似乎是比較統一檢視的方式,維護起來相對容易。

<Parent className="text-blue-600">
  <Child className="text-red-600">test</Child>
</Parent>

Tailwind CSS Atomic CSS css OOCSS 學不動了 CSS in JS styled-components