0%

初探 Shadcn UI:基於 Tailwind CSS + Radix UI 的元件合集

cover

GitHub - https://github.com/shadcn-ui/ui

What is Shadcn UI?

Shadcn UI 是基於 Tailwind CSS 底層封裝 Radix UI 的 React UI 元件集合,能夠支援 Next.js, Astro, Remix, Gatsby 等框架。

shadcn-ui/ui 專案於 2023 年 1 月發布到 GitHub,截至目前(2024 年 7 月)已有超過 65K 星星數,榮登 2023 JavaScript Rising Stars 榜首。

之所以能夠成為炙手可熱的開源專案,如 Shadcn 官方文件介紹所述:

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, Meterial UI, Ant Design 等說起。

透過元件庫提供的預設元件進行開發,就不需要從零開始造輪子,能夠統一介面樣式並實現 RWD 等需求,然而在實際使用元件庫時,會發現在客製化上有一定限制,經常會為了改一小部分樣式,而透過 !important:deep 等「魔改」的方式,來達到自訂義樣式需求,如此不只增加程式碼的複雜性,也不易後續維護使用。

如上所述,Shadcn UI 具備的種種特性,提供現有的元件庫解決方案,其優點如下:

  • 基於 Tailwind CSS 開發:學習曲線相對較低
  • 底層封裝 Radix UI:是一種 Headless UI,只包含功能和邏輯、提供完全無樣式、支援無障礙的 UI 元件庫,因此易於客製化
  • 可重用的元件集合:官方文件提供許多範例,可根據需求選擇元件、主題樣式,透過 CLI 或手動複製程式碼到專案中編輯使用
  • 輕量有彈性:不需一次安裝所有套件,需要時再透過 shadcn-ui 引入使用
  • 支援主流框架,如 Next.js, Remix, Vue

How to use Shadcn UI?

詳細步驟可參考官方文件,本篇以 Next.js 專案為例:

專案建置 Getting Start

  1. 透過 creat-next-app 指令建置 Next.js 專案
npx create-next-app@latest my-app --typescript --tailwind --eslint

next.js

  1. 執行 CLI shadcn-ui 初始專案
cd my-app
npx shadcn-ui@latest init
  1. 選擇系統初始樣式配置,後續也可在設定檔 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

建立好的 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
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>
  );
}
  1. tailwind.config.js 調整設定檔 theme.extend.fontFamily,即可根據需求自定義樣式
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 元件,或直接從官方文件手動複製
npx shadcn-ui@latest add button

安裝好的元件路徑會在 components 底下:@components/ui/button

button

  1. 即可引入專案使用
// app/page.tsx

import { Button } from "@/components/ui/button";

export default function Home() {
  return (
    <>
      <h1>Hello World</h1>
      <Button>Enter!</Button>
    </>
  )
}

demo-1

  1. 除了預設樣式,也可直接編輯元件程式碼,例如在 components/ui/button.tsx 檔案中新增自訂義樣式 newButton,即可在頁面使用
// 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

暗色主題 Dark Mode

由於 Shadcn UI 所有元件已內建 Dark Mode 樣式設定,因此只需搭配 Themes 來實作,而 Next.js 可搭配 next-themes 套件,詳細可參考 Dark mode 官網範例:

  1. 透過 CLI 安裝 next-themes
npm install next-themes
  1. 引入 next-themes 中的 ThemeProvider 來管理主題樣式:components/theme-provider.tsx
// 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>
}
  1. 將 ThemeProvider 引入至 root layout
// 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>
    </>
  )
}
  1. 建立能觸發切換深淺模式的 ModeToggle 按鈕,這裡可安裝 radix-ui/react-icons 引入 icon 使用
npm install @radix-ui/react-icons
  1. 新增 toggle button 到頁面,注意需加上 use client 才能引入 useTheme hook 使用
"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

小結

前陣子在開發 Next.js 專案時,是搭配使用 material-ui 這套 UI Framework,卻意外發現在過程中遇到許多痛點:

  • MUI 和 Tailwind 樣式需分開設定,可能發生樣式衝突,不易整合管理
  • 需注意部分元件為付費方案,如 MUI X Data Grid,免費版的 Data Grid 功能相對較少
  • MUI 能達到風格一致性的需求,但元件客製化不易,在開發上較缺乏彈性

因此藉此機會來研究 Shadcn UI,希望能改善開發上遇到的問題,後續再根據專案需求來導入合適的方案,畢竟任何工具沒有絕對好壞,而是根據使用情境來評估是否導入使用。

Reference

  • 為什麼 Shadcn UI 是 2023 年前端最熱門的開源專案?
  • Shadcn-ui : 美觀、無障礙、又能 100 % 客製化的「元件合集」
  • 在 2023 年屌爆了一整年的 shadcn/ui 用的 Headless UI 到底是何方神圣?