最近在玩 tailwind,紀錄一下整個使用的過程,以及與團員討論過程中的一些資訊。
開始使用 tailwind — 環境安裝
這裡以 next.js 來當作範例,或是參考官網的安裝過程 https://tailwindcss.tw/docs/guides
先安裝相關套件
$ yarn add -D tailwindcss@latest postcss@latest autoprefixer@latest
產生 tailwind.config.js / postcss.config.js 。如果已經有 postcss config 的話,就不用加 -p
。Next.js 內建使用 postcss 所以不需要另外對 postcss 做設定
$ npx tailwindcss init -p
這會產生一個基本的 postcss.config.js
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
如果你的 postcss.config 設定比較複雜,可以參考這裡
然後我們就可以到 CSS 檔案內把 tailwind 的東西弄進來,先到你的 globals.css (或任何你用的基本 css 檔)加入以下幾行。這幾行的前後仍然可以塞其他 CSS,最後產生出的 CSS 順序不會被更改。
/* ./styles/globals.css *//* 其他在 tailwind css 前的 css 內容 */@tailwind base;
@tailwind components;
@tailwind utilities;/* 其他在 tailwind css 前的 css 內容 */
並確認 pages/_app.js 有把這個 CSS import 進來
/* ./pages/_app.js */
import '../styles/globals.css'
然後 yarn dev
就可以開始開發了,試著在頁面上加入一些 tailwind class 玩玩看吧。
開發環境
為了開發方便,我們還需要做一些編輯器上的調整,這裡以 VSCode 為主,其他請見官網
- stylelint 調整為支援 tailwind
- tailwind 相關的語法高光
- 自動完成 tailwind utility class
先安裝 stylelint 相關套件
$ yarn add -D stylelint stylelint-config-recommended
然後修改 .stylelinrc
檔案,讓 stylelint 忽略 tailwind 的特殊語法 @tailwind
@apply
@layer
等
{
"extends": "stylelint-config-recommended",
"rules": {
"at-rule-no-unknown": [ true, {
"ignoreAtRules": [
"extends",
"tailwind",
"apply",
"layer",
"variants",
"responsive",
"screen"
]
}],
"block-no-empty": null
}
}
最後到編輯器內,安裝三個延伸模組
- Stylelint
- PostCSS Language Support 讓編輯器辨認得出 CSS 檔案中的 PostCSS 相關語法
- Tailwind CSS IntelliSense Tailwind class 自動完成及預覽
再來是進到 VSCode 的使用者設定或工作區設定,修改以下三個設定。因為 VSCode 內建 css/scss validator 會將 @tailwind
@layer
視為錯誤,因此需要關閉,改用 stylelint。
"css.validate": false, // Disable css built-in lint
"scss.validate": false, // Disable scss lint (optional if using scss)
"stylelint.enable": true, // Enable sytlelint
支援 tailwind 的開發環境到這裡就設定完成。
核心概念
使用 Tailwind 的時候必須要理解的核心概念,這裡講的大部分官網都有,如有不清楚的請以官網為準
Utility-First
用各式各樣 tailwind 提供的 utility class 來組出複雜的元件樣式,這些 utility class 就如同樂高積木一樣,提供基本的零組件,讓你建構出複雜的架構,這其實也很類似 functional programming 的概念。
那麼,這跟 inline style 有什麼不一樣?
- inline style 無法做到 responsive / hover / click 等狀態,這一定要寫在 css 檔內
- 透過 utility class 限制了使用數值,可以更輕易的與 design system 做搭配,後續要調整也會比較快。比如下圖是 border radius 的可用 class,後面的屬性可以透過設定檔做修改,一次套用到所有使用相對應 class 的地方
另外一個比較常討論的是可維護性,因為在元件上原本可能只需要幾個 class 一下子變成了十幾二十個,看起來應該不容易維護。這可以透過兩種方式解決
元件化
元件化的重點在於透過模板減少重複的程式碼,因此只要能夠好好的規劃,同樣重複出現的區塊都使用同一個模板,那麼要改樣式時不管需要調整幾個 class 也都只需要調整相對應的元件即可
@apply
一個好維護的專案必定會有足夠層度的元件化,當元件一多複雜度增加時,不可避免有一些 utitilty 的組合可能會在不同元件中需要共用。比如我們希望可點擊區域行為一致,顏色、背景色、hover 狀態都相同,可點擊區域可能是 button ,也有可能是 tab 或其他元件。如果不用 @apply 的話勢必得在每個元件上重複寫同樣的幾個 class,也導致未來可維護性降低因為修改時有可能會遺漏,因而偏離原本希望的行為一致的目標。
使用 @apply 其實就像是在寫一般我們常寫的,屬性比較複雜的 class,只是這個 class 的內容是由其他的 utility class 組起來。這裡要記得,寫這樣的 class 是為了方便共用,如果只有一個地方要用這樣的組合的話,我會選擇不使用 @apply 而是直接把相關的 utility class 寫在元件上,多一層 @apply 反而會造成閱讀上的負擔
透過 @apply 來產生複雜 class 還有個好處,因為底層都是 utility class,如果相關的設定調整了(比如以下範例的 font-semibold 的 weight),那麼上層是不需要做任何變動也會跟著調整。
<!-- 都是點擊區域 -->
<Button class="py-2 px-4 rounded-lg shadow-md font-semibold text-white bg-green-500 hover:bg-green-700">
按我一下
</Button><Dropdown class="py-1 px-3 font-semibold text-white bg-green-500 hover:bg-green-700" items={items} /><!-- 用 @apply 提取 Class -->
<Button class="py-2 px-4 rounded-lg shadow-md clickable">
按鈕
</Button><Dropdown class="py-1 px-3 clickable" items={items} /><style>
.clickable {
@apply font-semibold text-white bg-green-500 hover:bg-green-700;
}
</style>
RWD (responsive)
預設有五個斷點,可以由設定檔去調整這些斷點或者新增刪除斷點:
使用方式: class 加上斷點的前綴,再加上 :
(這也可以透過設定檔修改)以及 utility class 名,就可以讓該 utility class 在某個斷點以上的解析度生效,如下圖預設 w-16
,但在 md
(768px 以上) 時會被 md:w-32
蓋掉,在 lg
(1024px 以上) 會使用 lg:w-48
。使用時要注意,斷點順序預設就是上圖的順序,越下面的會蓋過前面的。
<!-- 寬度預設值設為 16, 中等螢幕設為 32, 大螢幕設為 48 -->
<img class="w-16 md:w-32 lg:w-48" src="...">
那如果只想要在某個範圍有特殊的樣式呢?比如上述例子做個修改,我只想要 md ~ lg 之間是 w-48
而其他時候 w-16
。只需要在更大的斷點上再把相對應的 class 套上去,藉此來蓋過前一個 class 的效果。
<!-- 寬度預設值設為 16, 中等螢幕設為 48, 大螢幕以上回到 16 -->
<img class="w-16 md:w-48 lg:w-16" src="...">
@screen
@screen
指令讓你依照斷點名稱建立相關的媒體查詢。
舉例來說,你想要有一個 640px
的 sm
斷點,並且你還需要撰寫一些自定義的 CSS 給這一個斷點。斷點名稱與上述圖表一樣,日後如果要調整可以透過設定定義 sm 的尺寸,或是直接搜尋 sm
一次替換成別的斷點名稱。
/* 原來的寫法 */
@media (min-width: 640px) {
/* ... */
}/* 新的寫法 */
@screen sm {
/* ... */
}
元素狀態 (variants)
如果需要讓元素在不同狀態下有不同的樣式,就需要了解 variants,前述的 responsive 也是一種特殊的 variant。先看看 variant 的列表:
大部分都與 CSS 的 psuedo-class 有對應關係,比如 hover
就對應到 CSS 的 :hover
。
下列表單使用了 focus
hover
兩種 variant,input 元素被 focus 時會產生 ring-2
的外圈,而 button hover 時則會變色 hover:bg-cyan-700
(到這裡試玩)
<form class="...">
<input class="border border-transparent bg-white text-gray-700 placeholder-gray-400 shadow-md text-base focus:outline-none focus:ring-2 focus:ring-cyan-600 focus:border-transparent ..." ...>
<button class="hover:bg-cyan-700 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2 focus:ring-offset-cyan-200 ..." type="button">
Sign up
</button>
</form>
用這些 variant 可以組合出許多的變化,但組合時必須要注意,跟 responsive 一樣,這些 variant 是有優先順序的,比如說,元素的狀態同時 focus 又 hover,且兩邊都定義了不同的背景顏色,那麼會呈現哪個?這時可以參考 預設的變化模式順序 ,排後面的代表優先權比較高會蓋過前面的,或者是透過設定檔自己改變順序,當然也可以自己新增 variant 。
variant 可以組合,其中 responsive 一定是第一個,dark 第二個(下一章節會介紹),後面接其他 variant,結構:
${responsive}:${dark}:${variants}:${utility-name}
一般 variant 只能選一個,所以才會有前面 focus 又 hover 需要處理排序的問題,但如果用的是 JIT mode 則可以使用多層 variant 像 sm:hover:active:disabled:opacity-75
這樣的 class。關於 JIT mode 後面會再介紹,在這裡先記得非 JIT 只能 responsive + 1 variant,而 JIT 沒有這個限制。
深色模式 Dark Mode
針對深色模式的支援,預設情況下, 只有 backgroundColor, borderColor, gradientColorStops, placeholderColor, and textColor 這幾個 utility class 有支援,如有其他需要要自己在設定新增。使用上:
<button class="lg:hover:bg-gray-800 lg:dark:hover:bg-white ...">
<!-- ... -->
</button>
深色模式的切換可以依據頁面 html 是否有 dark
這個 class 或者是瀏覽器的 prefers-color-scheme
設定,這可以在設定檔中修改:
//tailwind.config.js
module.exports = {
darkMode: 'class' | 'media',
// ...
}
用 class
這個選項會比較彈性些,就可以搭配下面這個簡單的 script,做到手動切換 淺色模式
、 深色模式
,和 根據作業系統的偏好設定
。
// 最好在頁面讀取或更換主題的時候增加一行到 `head`來避免 FOUC
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}// 當使用者選擇 淺色模式 時
localStorage.theme = 'light'// 當使用者選擇 深色模式 時
localStorage.theme = 'dark'// 當使用者選擇 根據作業系統偏好設定 時
localStorage.removeItem('theme')
補充:其實 dark 可以看成是另一種主題 (theme) 因此我們可以透過類似的做法來延伸做出多個主題,詳情可以看這裡
基底樣式
現有專案導入 tailwind 的可能特別需要注意這塊,基底樣式是 tailwind 預設會引入的,影響全域的樣式。Utility class 因為有使用才會有效果,所以對現有專案的影響不大。這裡可以看到預設基底樣式 preflight 的設定,不喜歡的話也可以從設定中移除。
當然,要自訂基底樣式也是可以的
@tailwind base;@layer base {
h1 {
@apply text-2xl;
}
h2 {
@apply text-xl;
}
h3 {
@apply text-lg;
}
a {
@apply text-blue-600 underline;
}
}@tailwind components;
@tailwind utilities;
運作機制
核心概念都了解後,先來講 tailwind 的 css 運作機制,關於運作機制可以分成兩塊來看,引入跟生成。
引入
以 next.js 為例:
- css 的引入點在
pages/_app.js
,從這檔案引入了globals.css
- 引入的
globals.css
會被 PostCSS 處理 - PostCSS 內的 tailwind plugin 看到關鍵字
@tailwind
即會把base
component
utility
三層各自的 css 產生出來,或是看到其他關鍵字如@layer
@screen
等進行相對應的 CSS 生成動作,生成的過程參考下一段落。如想更深入的了解底層的轉換機制,可以看看這篇 ,或是找 tailwind 的原始碼來參考。 - 頁面上即有了相對應的 CSS
生成
生成即是把原始的 CSS 變成給瀏覽器用的 CSS ,這部分分成一般開發模式、生產模式、以及 JIT 模式
- 一般開發模式:會引入已經預先建置好的 tailwind.css ,裡面包含了各種 utility class 的組合,所以很大一包,但在開發模式時並無影響,就由開發者自由取用。
- 生產模式:開發模式用的 tailwind.css 的未壓縮大小是 3.5 MB ,明顯不適合生產環境使用,因此上到生產模式時會經過一層 purge 的關卡,把沒有使用到的 class 全部清理乾淨,這裡的清理標準是透過正則表達式,去掃描對應的模板檔案 (jsx, html 等)。因此不建議動態生成 utility class 否則在清理過程時會無法辨認有使用到哪些 utility class 造成要用到的 class 不會出現在生產模式。另外針對 html tag 的樣式不會被清除,如有特別需求比如白名單之類,則可以在設定中設定。
- JIT 模式:動態依據頁面上有的 class 去產生出相對應的 class,相較原有模式多了許多彈性,生產模式的建置速度也比較快(其實 JIT 開發模式就等於生產模式),如前述的可以多個 variant 相疊。不過目前尚處於實驗階段,可以玩玩但需要隨時注意規格上的改動。
RAILS?
Rails 6 之後可以使用 DHH (Rails 作者) 做的 https://github.com/rails/tailwindcss-rails ,5 之前就要自己想辦法,https://github.com/rails/tailwindcss-rails/issues/50。
附上一個 migration guide https://www.honeybadger.io/blog/tailwind-css-rails/
參考資料
- 中文化官方網站 https://tailwindcss.tw/
- 相關資料彙整 https://github.com/aniftyco/awesome-tailwindcss
- 電子週報 https://www.getrevue.co/profile/tailwind-weekly
- 熱騰騰的鐵人賽文章 https://ithelp.ithome.com.tw/users/20138853/ironman/3928
- 渲染順序 https://hackmd.io/@lalarabbits/Rendering_tailwind
- 社群 https://www.facebook.com/groups/974885506586730
- Playground https://play.tailwindcss.com/