# 初探 Shadcn UI:基於 Tailwind CSS + Radix UI 的元件合集 [![hackmd-github-sync-badge](https://hackmd.io/VYVaxfcbSh-eGfvzqIpKdQ/badge)](https://hackmd.io/VYVaxfcbSh-eGfvzqIpKdQ) [toc] ![cover](https://hackmd.io/_uploads/B1RYAaBtA.png) > 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 ``` ![next.js](https://hackmd.io/_uploads/rk3c9__tA.png) 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 ``` ![init config](https://hackmd.io/_uploads/HJ1SsudtC.png) 建立好的 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` ![button](https://hackmd.io/_uploads/S1LnfY_F0.png) 2. 即可引入專案使用 ```typescript= // app/page.tsx import { Button } from "@/components/ui/button"; export default function Home() { return ( <> <h1>Hello World</h1> <Button>Enter!</Button> </> ) } ``` ![demo-1](https://hackmd.io/_uploads/H1cGNtdtC.png) 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> </> ) } ``` 結果如下: ![button](https://hackmd.io/_uploads/ryOrYj_tR.png) ### 暗色主題 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> </> ) } ``` 效果如下: ![demo-dark](https://hackmd.io/_uploads/Hy4Jv6uK0.gif) ## 小結 前陣子在開發 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)