iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
自我挑戰組

30 天打造 Accessible UI Kit:從 shadcn/ui 到自己的 Design System系列 第 7 篇

Day 7: 第一個元件 Button:從 shadcn/ui 變成自己的 Button

  • 分享至 

  • xImage
  •  

前幾天已經把 shadcn/ui 的 Button 拆開來看過,也建立了自己的 Design Tokens。
到了今天,終於可以開始動第一個真正的元件:Button。

這個 UI Kit 一開始就希望可以同時提供給兩種環境使用:

React + TypeScript

Legacy
HTML + CSS

開始改樣式以前,我想先決定一件更重要的事:

在這個 UI Kit 裡,一顆 Button 到底應該怎麼被描述?

先看看 shadcn/ui 給我的 Button

const buttonVariants = cva(
  "...",
  {
    variants: {
      variant: {
        default: "...",
        outline: "...",
        secondary: "...",
        ghost: "...",
        destructive: "...",
        link: "...",
      },
      size: {
        default: "...",
        xs: "...",
        sm: "...",
        lg: "...",
        icon: "...",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

會這樣組成一個button

<Button variant="outline" size="sm">
  取消
</Button>

第一個想改的是:

variant="default"

它沒有錯,但開始建立自己的 Design System 後,我比較希望 API 本身就能表達設計語意。

例如:

<Button variant="primary">
  儲存
</Button>

看到 primary,就知道它代表主要操作。

因此第一版先把 Button variant 定成:

primary
secondary
outline
ghost
destructive
link

size 也整理成比較一致的:

sm
md
lg

icon-sm
icon-md
icon-lg

使用時就可以寫:

<Button>儲存</Button>

<Button variant="secondary">
  上一步
</Button>

<Button variant="outline" size="sm">
  取消
</Button>

React 有 variant,那舊系統呢?

React 可以這樣描述一顆 Button:

<Button variant="primary" size="md">
  儲存
</Button>

但原生html不會跑React
如果未來舊系統另外變成:

<button class="btn btn-primary btn-md">
  儲存
</button>

我們其實又開始建立第二套規則了。

所以我希望 React 和 Legacy 至少先有一套共同的語言。

最後決定使用 data-cui-*:

<button
  data-cui-slot="button"
  data-cui-variant="primary"
  data-cui-size="md"
>
  儲存
</button>

它們分別表示:

  • data-cui-slot="button" → 這是一個 CUI Button
  • data-cui-variant="primary" → 這是一個 Primary Button
  • data-cui-size="md" → 使用 Medium 尺寸

於是 React 與 Legacy 雖然使用方式不同,描述的卻是同一件事

React API                         HTML Contract

<Button                           <button
  variant="primary"                 data-cui-slot="button"
  size="md"                         data-cui-variant="primary"
>                                   data-cui-size="md"

讓 React 自己產生這份 Contract

function Button({
  className,
  variant = "primary",
  size = "md",
  ...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
  return (
    <ButtonPrimitive
      data-slot="button"
      data-cui-slot="button"
      data-cui-variant={variant}
      data-cui-size={size}
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  )
}

React寫這樣:

<Button variant="outline" size="lg">
  取消
</Button>

實際產生的 DOM 就會包含:

<button
  data-slot="button"
  data-cui-slot="button"
  data-cui-variant="outline"
  data-cui-size="lg"
>
  取消
</button>

data-slot 先保留給 shadcn/ui 元件內部的組合與 selector 使用
data-cui-* 則是自己的 UI Kit 對外定義的 contract


Button 還是使用前幾天建立的 Tokens

Day5 做的 Design Tokens,現在也開始派上用場了。

primary:
  "bg-primary text-primary-foreground hover:bg-primary/80"

其中 primary 最後會一路對應到自己的 semantic token:

Button
↓
bg-primary
↓
--primary
↓
--cui-color-primary
↓
Primitive Color

所以Button並不知道藍色到底是什麼藍 色號是什麼

它只知道:
我是 Primary Button,所以我要使用 Primary Color
這也就是前面把 Primitive Token 和 Semantic Token 分開的原因


Button 不只有「長什麼樣」

Button 還有另一件很重要的事:狀態。

目前保留並測試:

hover
active
focus-visible
disabled

例如 focus:

focus-visible:border-ring
focus-visible:ring-3
focus-visible:ring-ring/50

disabled:

disabled:pointer-events-none
disabled:opacity-50

active:

active:not-aria-[haspopup]:translate-y-px

這也是我覺得直接從成熟的 primitive 開始很方便的地方。

如果一開始自己寫:

<button className="bg-blue-600 text-white">

畫面很快就會有一顆漂亮的藍色按鈕。

但真正的 Button 還要考慮鍵盤操作、focus、disabled 等行為。
UI Kit 做的並不只是「元件看起來一致」,也希望這些互動規則可以一起被帶進每個系統


那 Legacy CSS 呢?

做到這裡有一個很明顯的問題。
既然已經定義:

<button
  data-cui-slot="button"
  data-cui-variant="primary"
  data-cui-size="md"
>

是不是現在就可以寫:

[data-cui-slot="button"] {...}

[data-cui-variant="primary"] {...}

我這次刻意沒有做。

因為目前 React 的 Button Rules 在:

const buttonVariants = cva(...)

如果現在又人工維護:

[data-cui-variant="primary"] {...}

架構就會變成:

                Button
               /      \
              /        \
       CVA / Tailwind   CSS
            ↓            ↓
          React        Legacy

也就是兩份 source of truth

Primary Button 改 padding,要改兩次
Focus 樣式調整,要改兩次
新增 variant,又要記得兩邊一起新增

這正是 Day 6 想避免的事情。


所以今天其實只完成了一半?

某種程度上,是 😂
目前完成的是:


                  Button Contract
                        │
            ┌───────────┴───────────┐
            ▼                       ▼
        React API               Legacy HTML
            │                       │
     variant="primary"       data-cui-variant
       size="md"              data-cui-size
            │                       │
            └───────────┬───────────┘
                        │
                 Shared Language

React 已經有實際 styling。
Legacy 則已經知道「怎麼描述同一顆 Button」,但還沒有真正取得 Button CSS。

接下來真正要解的問題會是:

                 Button Definition
                        │
                      Build
                 ┌──────┴──────┐
                 ▼             ▼
               React        CDN Assets
                 │             │
                 ▼             ▼
            React App       HTML+CSS

也就是:

怎麼讓同一份 Button 規則,同時輸出給 React 和 Legacy 使用?
這個問題比另外複製一份 CSS 麻煩一點,但也正是這個 UI Kit 想解決的核心問題之一。

所以先不急著偷懶複製 CSS,留給後面的自己處理 😆

今天完成了什麼?

Day 7 最後,第一顆 CUI 元件已經有了自己的基本規則:

Button

Variants
├─ primary
├─ secondary
├─ outline
├─ ghost
├─ destructive
└─ link

Sizes
├─ sm
├─ md
├─ lg
├─ icon-sm
├─ icon-md
└─ icon-lg

Contract
├─ data-cui-slot
├─ data-cui-variant
└─ data-cui-size

States
├─ hover
├─ active
├─ focus-visible
└─ disabled

從畫面上看,今天可能只是多了幾顆 Button。

但從架構上來看,這是第一次把前面幾天的東西真正串起來:

Design Tokens
      ↓
Component Rules
      ↓
Component Contract
      ↓
React Button
      ↓
未來的 CDN Output

所以今天做的其實不是「把 shadcn/ui Button 換成自己的顏色」。

而是開始回答:

一個元件要怎麼成為這套 Design System 的元件?

第一顆 Button 完成!
接下來,就可以繼續看看怎麼讓這些規則不只活在 React 裡


上一篇
Day 6: Tailwind CSS + SCSS,可以一起用嗎?
下一篇
Day 8: Button 不只是能按:從鍵盤操作開始做無障礙
系列文
30 天打造 Accessible UI Kit:從 shadcn/ui 到自己的 Design System 共 20 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言