Shadcn-ui : 美觀、無障礙、又能 100 % 客製化的「元件合集」
2023 年三月釋出,截至目前已超過 60K Github 星星,更獲得了 2023 JavaScript Rising Stars 調查年度第一名 ⭐
建議先備知識
- Tailwind CSS 基本概念
- React 基本概念與實作經驗
前言
在開發前端介面時,總是會遇到像是按鈕、輸入框、警告視窗等等這些一再出現的元件,若每次都要從頭開始造輪子,想必要花費大量的時間和精力,尤其在無障礙體驗方面,靠一己之力很難做到面面俱到。這時候,運用元件庫就能有效提升產品品質和開發速度。市面上常見到 React 元件庫有 Material UI、Ant Design、Chakra UI 等等,而 Vue 生態系則有 Quasar、Vuetify 等等。
今天要來介紹的是一個由 Vercel 的 design engineer 所推出的「元件合集」(component collection),Shadcn-ui (不是 shad-CDN 🫠)。根據官方的說法,之所以將自己定位成「元件合集」而非「元件庫 (component library)」的原因在於:與一般的元件庫不同,Shadcn-ui 並非以 dependency 的形式安裝在專案中,相反的,所有元件的程式碼都會直接存放在該專案的元件資料夾中,可以任意的修改並客製化。
值得一提的是,Shadcn-ui 其實是建立於另外一個元件庫 Radix UI 的 primitive 元件上 (意指沒有提供預設樣式的元件),這些 primitive 元件提供了完整的無障礙體驗與元件基礎 API,而 Shadcn-ui 在這個基礎上透過 Tailwind CSS 為這些元件包裝了基礎樣式,讓開發者能更快速的打造兼具美感和實用性的 prototype。
Shadcn UI 的一些特色
完整的無障礙體驗
Shadcn UI 是建立於 Radix UI 上,而 Radix UI 所標榜的最大特色之一就是他們的 Accessibility:所有元件都遵守 WAI-ARIA 標準規範,且有通過跨瀏覽器及多種輔助科技的測試。有興趣了解更多的人可以讀一讀 Radix UI 的文件。
程式碼完全由你控制
前面有提到 Shadcn ui 與一般的元件庫最大的不同在於,所有的元件都可以直接在專案中進行編輯。以下面這個 button 元件為例,當我們透過 CLI 指令將 button 新增到我們的專案中,會看到在 components 資料夾下面多了一個 ui 資料夾,其中就能找到剛剛新增的 button 元件。
如同我們自行定義的任何元件,我們可以針對這個 button 進行任何修改,例如加上新的 variant 等等。此外,Shadcn ui 是由 TypeScript 編寫,我們新增的樣式也都會在自動完成的下拉選單中出現。
複製貼上不是問題
Shadcn ui 首頁上面擺明了寫著:這是一個你可以輕鬆 copy and paste 的元件合集。不只所有的元件都是開源的、可以一鍵複製,在「Examples」這個頁面中還提供了儀表版 (dashboard)、多種卡片、資料表格、登入頁面等等精美實用的範例。這些全部都能透過「複製貼上」來應用到自己的專案中,省下從頭設計 UI 的麻煩,對於獨自開發 side project 的人來說簡直是天上掉下來的禮物✨。
多種主題可直接套用
除了有許多精美範例可以使用,在「Themes」頁面我們可以選擇要套用在專案中的色彩主題,並即時的看到這些主題應用在元件上的模樣。只要點選「Copy code」並將 CSS variables 的設定貼到我們的 CSS 檔案中,就套用完畢啦!
結合 React Hook Form 和 Zod 的表單
雖然 Shadcn ui 主要是建立在 Radix UI 上,但對於某些元件的處理,他仍提供了一套 opinionated 的做法。例如前端常見的 Form,在實務上通常會結合一些表單驗證 library 或是 schema validation library 來做更完善的資料和錯誤處理。
在文件中,他們提到一個好的表單應該具有以下幾個基本要求:合理的結構與語意、易於操作(尤其針對鍵盤使用者)、提供正確的 ARIA 屬性與標籤、支援客戶端及伺服器端驗證、風格與網頁其他部分保持一致。Shadcn ui 在這方面選擇使用 React Hook Form 和 Zod 這兩個 library 來讓它的表單元件更加 production-ready。
React Hook Form 是 npm 上下載次數最高的 React 表單驗證套件,提供一套簡易的 API 來讓在 React 中操作 uncontrolled components 變得更容易,若要使用第三方套件的 controlled component 也有相關的 API 能使用。RHF 更提供了完整的 resolvers 套件與超過 10 種不同的 schema validation library 做整合。
Zod,根據作者的說明,是一個 TypeScript-first 的驗證、定義 schema 的函式庫。它會依據你給定的格式創造 schema,還能自動推斷出靜態的 typescript type。以表單的 schema 為例,在撰寫表單資料的相關程式碼時,就會有型別提示與自動完成的功能。它所提供的驗證 API 相當多樣化,也能輕易地客製化錯誤訊息。
從上方的鍵盤操作範例可以看到,Shadcn ui 所提供的表單元件已經整合了焦點控制、錯誤訊息的顯示與錯誤狀態時的基本樣式變化,我們只需要定義好自己的 schema 並串接好 API 就能完成美觀及實用兼具的表單。
結合 Tanstack Table 的表格
另外一個值得一提的則是 Table 元件。從樣式設定、無障礙操作、排序過濾搜尋等等,Table 無疑是大部分前端工程師的噩夢,想到要自己從頭開始做就覺得頭痛🤯。大多數的元件庫都會提供已經做好基本功能的 Table,不過,Shadcn ui 有點不同,若仔細觀察它所提供的程式碼,Table 元件只不過是經過樣式包裝的原生 HTML 元素罷了。
但是在文件中,他們提供了簡單易懂的教學,手把手帶你整合 Tanstack Table,告訴你如何定義表格欄位、樣式設定、加入分頁、搜尋、過濾等功能。Tanstack Table 本身是一個用來製作複雜表格和資料欄位的 Headless UI (前身為 React Table,不過現在整合了各框架的 package),功能非常強大,有內建的分頁、搜尋、排序功能,並能做到高度客製化。不過必須要說的是,他們的官方文檔對於新手來說不太友善,API 的解釋都蠻簡略的,且很難找到自己當下需要的內容🥲。
幸好,Shadcn ui 提供的教學,以及 Examples 中的表格範例,已大大降低了 Tanstack Table 的上手門檻,並足夠讓人做出具有多種功能的基本表格,只需要在卡關的時刻額外搜尋資源就好。
開始使用 Shadcn ui
基本安裝設定 (Vite)
npm create vite@latest
2. 接著下載 Tailwind CSS 以及相關的套件,並生成 config 檔案
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
3. 在你的 tsconfig.json
檔案中新增以下設定,確保解析元件路徑時不會出錯
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
4. 更新你的 vite.config.ts
檔案,確保解析元件路徑時不會出錯
# (so you can import "path" without error)
npm i -D @types/node
import path from "path"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})
5. 執行 shadcn-ui
CLI 指令來初始化 Shadcn ui
npx shadcn-ui@latest init
6. 根據需求回答以下問題以完成初始化
Would you like to use TypeScript (recommended)? no / yes
Which style would you like to use? › Default
Which color would you like to use as base color? › Slate
Where is your global CSS file? › › src/index.css
Do you want to use CSS variables for colors? › no / yes
Where is your tailwind.config.js located? › tailwind.config.js
Configure the import alias for components: › @/components
Configure the import alias for utils: › @/lib/utils
Are you using React Server Components? › no / yes (no)
7. 完成設定後就能開始新增元件
舉例來說以下的指令會將 button 元件檔案新增到你的 components/ui
資料夾中:
npx shadcn-ui@latest add button
我們可以在頁面中 import 剛剛新增的 Button 元件如下:
import { Button } from "@/components/ui/button"
export default function Home() {
return (
<div>
<Button>Click me</Button>
</div>
)
}
設定暗色模式 (Vite)
Shadcn ui 的所有元件都已經有內建的暗色模式的樣式設定,我們不需要再額外新增暗色模式的樣式。搭配 「Themes」中的 CSS Variables 設定,只要再加入以下幾個簡單的步驟就可以有美美的暗色模式可以用啦。
(以下以 Vite 專案為例,官方還有提供 Next.js 和 Astro 文檔)
- 設定
theme-provider
檔案如下
import { createContext, useContext, useEffect, useState } from "react"
// 預設有三種模式設定
type Theme = "dark" | "light" | "system"
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
)
// 初始化的時候會依據客戶端是否有設定模式偏好來決定顯示的模式
useEffect(() => {
const root = window.document.documentElement
root.classList.remove("light", "dark")
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light"
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)
},
}
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider")
return context
}
2. 在 App.tsx
新增 ThemeProvider
,傳入預設模式以及用來儲存主題的 storage key 名稱
import { ThemeProvider } from "@/components/theme-provider"
function App() {
return (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
{children}
</ThemeProvider>
)
}
export default App
3. 新增 mode-toggle
元件,讓使用者在應用程式中切換模式
// 也可以換成任何喜歡的 icon 包
import { Moon, Sun } from "lucide-react"
import { Button } from "@/components/ui/button"
// 預設的 mode toggle 元件是有下拉選單的
// 因此會需要載入 dropdown-menu 元件,不過可依照需求修改
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
// 載入剛剛在 theme-provider 中設定的 custom hook
import { useTheme } from "@/components/theme-provider"
export function ModeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
我的 side project
我的第三個 side project Issuezy 就是用 Shadcn ui 打造的!比起在第一個 side project 中,花了將近 1/3 的開發時間在設計和開發 UI 樣式,第三個 project 只花不到一周的時間就組裝了大部分的元件,剩下的時間就可以專注在串接功能上👌。歡迎到 Github Repo 上面參考 Readme 中的截圖,或是到 這邊 玩玩看 Live Demo。
結語
除了上述提到的特點,在這篇文章中網友還提到 Shadcn ui 不像其他元件庫,若要使用單一元件的話就要一次導入整包 package,可以直接透過 CLI 工具新增需要的元件,並無痛整合到現有的專案中。唯一的限制大概就是專案必須使用 Tailwind CSS。
近幾年 Tailwind CSS 的崛起催生了許多建立於 Tailwind CSS 之上的元件庫 (例如本文所提到的 Shadcn-ui,還有 Daisy UI、Tailwind UI、Flowbite 等等族繁不及備載),它易於客製化與複製的特性,還逐漸與生成式 AI 工具的風潮結合,例如 versel 所推出的 v0,可以透過指令來讓 AI 直接生成需要的元件。
就個人的使用經驗與觀察,隨著工具鍊的成熟和元件庫的擴展,Tailwind CSS 很有可能變成未來主流的 CSS 解決方案,而我也樂見其成。Tailwind 團隊在今年六月的時候還辦了一場實體 conference (Tailwind Connect),就知道這股潮流只有越滾越大的趨勢。還沒有跟上 Tailwind 風潮的你,在等什麼呢?😌
(結果變成了 Tailwind 推坑文章了嗎…)