# 初探 Shadcn UI:基於 Tailwind CSS + Radix UI 的元件合集
[](https://hackmd.io/VYVaxfcbSh-eGfvzqIpKdQ)
[toc]

> GitHub - https://github.com/shadcn-ui/ui
## What is Shadcn UI?
Shadcn UI 是基於 [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss) 底層封裝 [Radix UI](https://github.com/radix-ui/primitives) 的 React UI 元件集合,能夠支援 [Next.js](https://ui.shadcn.com/docs/installation/next), [Astro](https://ui.shadcn.com/docs/installation/astro), [Remix](https://ui.shadcn.com/docs/installation/remix), [Gatsby](https://ui.shadcn.com/docs/installation/gatsby) 等框架。
[shadcn-ui/ui](https://github.com/shadcn-ui/ui) 專案於 2023 年 1 月發布到 GitHub,截至目前(2024 年 7 月)已有超過 65K 星星數,榮登 [2023 JavaScript Rising Stars](https://risingstars.js.org/2023/en) 榜首。
之所以能夠成為炙手可熱的開源專案,如 [Shadcn 官方文件](https://ui.shadcn.com/docs)介紹所述:
> Beautifully designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source.
> (您可以複製貼上設計精美的元件至應用程式中。無障礙、可客製化且開源。)
Shadcn UI 並不是一個 component library(元件庫),而是可重用元件的「集合」。換言之,Shadcn UI 並不會以 dependency 的形式出現在 `pacakge.json`,使用者可以直接把原始碼複製貼上到專案中,也能自行修改客製化。
## Why Shadcn UI?
Shadcn UI 在前端生態圈崛起的原因,可從過去我們所熟悉的 UI Framework,如 [Bootstrap](https://getbootstrap.com/), [Meterial UI](https://mui.com/), [Ant Design](https://ant.design/) 等說起。
透過元件庫提供的預設元件進行開發,就不需要從零開始造輪子,能夠統一介面樣式並實現 RWD 等需求,然而在實際使用元件庫時,會發現在客製化上有一定限制,經常會為了改一小部分樣式,而透過 `!important`、`:deep` 等「魔改」的方式,來達到自訂義樣式需求,如此不只增加程式碼的複雜性,也不易後續維護使用。
如上所述,Shadcn UI 具備的種種特性,提供現有的元件庫解決方案,其優點如下:
+ 基於 Tailwind CSS 開發:學習曲線相對較低
+ 底層封裝 [Radix UI](https://www.radix-ui.com/primitives):是一種 [Headless UI](https://headlessui.com/),只包含功能和邏輯、提供完全無樣式、支援無障礙的 UI 元件庫,因此易於客製化
+ 可重用的元件集合:官方文件提供許多範例,可根據需求選擇元件、主題樣式,透過 CLI 或手動複製程式碼到專案中編輯使用
+ 輕量有彈性:不需一次安裝所有套件,需要時再透過 `shadcn-ui` 引入使用
+ 支援主流框架,如 [Next.js](https://ui.shadcn.com/docs/installation/next), [Remix](https://ui.shadcn.com/docs/installation/remix), [Vue](https://www.shadcn-vue.com/) 等
## How to use Shadcn UI?
詳細步驟可參考官方文件,本篇以 [Next.js](https://ui.shadcn.com/docs/installation/next) 專案為例:
### 專案建置 Getting Start
1. 透過 `creat-next-app` 指令建置 Next.js 專案
```
npx create-next-app@latest my-app --typescript --tailwind --eslint
```

2. 執行 CLI `shadcn-ui` 初始專案
```
cd my-app
npx shadcn-ui@latest init
```
3. 選擇系統初始樣式配置,後續也可在設定檔 `components.json` 做調整
```
Which style would you like to use? › Default
Which color would you like to use as base color? › Slate
Do you want to use CSS variables for colors? › no / yes
```

建立好的 Next.js 專案架構可參考如下,其中引入的 Shadcn 元件會放在 `compoent/ui` 資料夾底下:
```
.
├── app
│ ├── layout.tsx
│ └── page.tsx
├── components
│ ├── ui // 引入的元件可在專案中進行編輯
│ │ ├── alert-dialog.tsx
│ │ ├── button.tsx
│ │ ├── dropdown-menu.tsx
│ │ └── ...
│ ├── main-nav.tsx // 將引入的元件進一步客製化元件
│ ├── page-header.tsx
│ ├── alert.tsx
│ ├── sidebar.tsx
│ └── ...
├── lib
│ └── utils.ts
├── styles // 自定義樣式
│ └── globals.css
├── next.config.js
├── package.json
├── postcss.config.js
├── tailwind.config.js // Tailwind 設定檔
└── tsconfig.json
```
### 新增字型 Font
1. 引入 font 字型至 root layout
```typescript=
import type { Metadata } from "next";
import "./globals.css";
import { Inter as FontSans } from "next/font/google"
import { cn } from "@/lib/utils"
const fontSans = FontSans({
subsets: ["latin"],
variable: "--font-sans",
})
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={cn(
"min-h-screen bg-background font-sans antialiased",
fontSans.variable
)}>
{children}
</body>
</html>
);
}
```
2. 在 `tailwind.config.js` 調整設定檔 `theme.extend.fontFamily`,即可根據需求自定義樣式
```javascript=
const { fontFamily } = require("tailwindcss/defaultTheme")
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"],
theme: {
extend: {
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans],
},
},
},
}
```
### 新增按鈕 Button
1. 透過 CLI 安裝 button 元件,或直接從[官方文件](https://ui.shadcn.com/docs/components/button)手動複製
```
npx shadcn-ui@latest add button
```
安裝好的元件路徑會在 components 底下:`@components/ui/button`

2. 即可引入專案使用
```typescript=
// app/page.tsx
import { Button } from "@/components/ui/button";
export default function Home() {
return (
<>
<h1>Hello World</h1>
<Button>Enter!</Button>
</>
)
}
```

3. 除了預設樣式,也可直接編輯元件程式碼,例如在 `components/ui/button.tsx` 檔案中新增自訂義樣式 `newButton`,即可在頁面使用
```typescript=
// app/page.tsx
import { Button } from "@/components/ui/button";
export default function Home() {
return (
<>
<h1>Hello World</h1>
<Button variant="outline">outline</Button>
<Button variant="link">link</Button>
<Button variant="newButton">newButton</Button>
</>
)
}
```
結果如下:

### 暗色主題 Dark Mode
由於 Shadcn UI 所有元件已內建 Dark Mode 樣式設定,因此只需搭配 Themes 來實作,而 Next.js 可搭配 `next-themes` 套件,詳細可參考 [Dark mode](https://ui.shadcn.com/docs/dark-mode/next) 官網範例:
1. 透過 CLI 安裝 `next-themes`
```
npm install next-themes
```
2. 引入 `next-themes` 中的 ThemeProvider 來管理主題樣式:`components/theme-provider.tsx`
```typescript=
// components/theme-provider.tsx
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
```
3. 將 ThemeProvider 引入至 root layout
```typescript=
// app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider"
export default function RootLayout({ children }: RootLayoutProps) {
return (
<>
<html lang="en" suppressHydrationWarning>
<head />
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
</>
)
}
```
4. 建立能觸發切換深淺模式的 ModeToggle 按鈕,這裡可安裝 `radix-ui/react-icons` 引入 icon 使用
```
npm install @radix-ui/react-icons
```
5. 新增 toggle button 到頁面,注意需加上 `use client` 才能引入 `useTheme` hook 使用
```typescript=
"use client"
import * as React from "react"
import { useTheme } from "next-themes"
import { MoonIcon, SunIcon } from "@radix-ui/react-icons"
import { Button } from "@/components/ui/button";
export default function Home() {
const { theme, setTheme } = useTheme();
return (
<>
<h1>Hello World</h1>
<Button variant="outline">outline</Button>
<Button variant="link">link</Button>
<Button variant="newButton">newButton</Button>
<div>
<Button variant="outline" size="icon" onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
<SunIcon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<MoonIcon 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>
</div>
</>
)
}
```
效果如下:

## 小結
前陣子在開發 Next.js 專案時,是搭配使用 [material-ui](https://github.com/mui/material-ui) 這套 UI Framework,卻意外發現在過程中遇到許多痛點:
+ MUI 和 Tailwind 樣式需分開設定,可能發生樣式衝突,不易整合管理
+ 需注意部分元件為付費方案,如 [MUI X Data Grid](https://mui.com/x/react-data-grid/),免費版的 Data Grid 功能相對較少
+ MUI 能達到風格一致性的需求,但元件客製化不易,在開發上較缺乏彈性
因此藉此機會來研究 Shadcn UI,希望能改善開發上遇到的問題,後續再根據專案需求來導入合適的方案,畢竟任何工具沒有絕對好壞,而是根據使用情境來評估是否導入使用。
## Reference
+ [為什麼 Shadcn UI 是 2023 年前端最熱門的開源專案?](https://medium.com/%E6%89%8B%E5%AF%AB%E7%AD%86%E8%A8%98/why-shadcn-ui-is-so-popular-in-2023-0f95e66f3ddc)
+ [Shadcn-ui : 美觀、無障礙、又能 100 % 客製化的「元件合集」](https://medium.com/@Kelly_CHI/shadcn-ui-tailwind-components-6fd4f1959147)
+ [在 2023 年屌爆了一整年的 shadcn/ui 用的 Headless UI 到底是何方神圣?](https://juejin.cn/post/7344719913019277323)