Radix UI: React Headless UI 元件庫
在現代 React 開發中,出現了一種新的趨勢:Headless UI(無頭元件)。這類元件庫提供完整的互動邏輯和無障礙功能 (Accessibility / a11y),但完全不提供樣式。
Radix UI 就是其中的佼佼者。它讓你不用重新造輪子去處理複雜的像是 Dialog 焦點鎖定、Dropdown 鍵盤導航等問題,同時又給你 100% 的樣式控制權(這讓它成為 Tailwind CSS 的最佳拍擋)。
安裝
Radix UI 是模組化的,你可以只安裝你需要的元件。
npm install @radix-ui/react-dialog @radix-ui/react-popover @radix-ui/react-switch
核心優勢 (Why Radix?)
Radix 的元件通常由多個部分 (Parts) 組成,你需要依照結構組合它們。這種模式雖然比傳統元件庫繁瑣一點,但帶來了極大的靈活性。
常用元件範例 (Common Primitives)
除了 Dialog,以下是幾個在開發中最常使用的元件:
Popover (氣泡對話框)
不同於 Dialog,Popover 是「非模態 (Non-modal)」的,使用者可以點擊外部關閉它,且背景不會變黑。常用於過濾器選單、更多選項。
import * as Popover from '@radix-ui/react-popover';
<Popover.Root>
<Popover.Trigger>更換顏色</Popover.Trigger>
<Popover.Portal>
<Popover.Content className="bg-white p-4 shadow-lg rounded">
選擇你喜歡的顏色...
<Popover.Arrow className="fill-white" />
</Popover.Content>
</Popover.Portal>
</Popover.Root>;
Tooltip (提示文字)
當滑鼠懸停時顯示的輔助說明。
import * as Tooltip from '@radix-ui/react-tooltip';
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button className="icon-btn">+</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="bg-black text-white px-2 py-1 text-sm rounded">
新增項目
<Tooltip.Arrow className="fill-black" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>;
Accordion (手風琴)
常見的收合/展開列表。
import * as Accordion from '@radix-ui/react-accordion';
<Accordion.Root type="single" collapsible>
<Accordion.Item value="item-1">
<Accordion.Header>
<Accordion.Trigger>常見問題 1</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>這是問題 1 的答案內容。</Accordion.Content>
</Accordion.Item>
</Accordion.Root>;
Dialog (對話框) 範例
一個無障礙的 Modal 其實非常難寫(要處理 Esc 關閉、點擊遮罩關閉、Focus Trap、移除 Scrollbar 等)。Radix 幫你處理了所有這些。
import * as Dialog from '@radix-ui/react-dialog';
import { Cross2Icon } from '@radix-ui/react-icons';
import './dialog.css'; // 這裡定義你的樣式 (或使用 Tailwind)
export default () => (
<Dialog.Root>
<Dialog.Trigger asChild>
<button className="Button violet">編輯個人資料</button>
</Dialog.Trigger>
<Dialog.Portal>
{/* 遮罩層 */}
<Dialog.Overlay className="DialogOverlay" />
{/* 內容層 */}
<Dialog.Content className="DialogContent">
<Dialog.Title className="DialogTitle">編輯個人資料</Dialog.Title>
<Dialog.Description className="DialogDescription">
在這裡修改你的資料,完成後點擊儲存。
</Dialog.Description>
<fieldset className="Fieldset">
<label className="Label" htmlFor="name">
姓名
</label>
<input className="Input" id="name" defaultValue="Mike Lee" />
</fieldset>
<div style={{ display: 'flex', marginTop: 25, justifyContent: 'flex-end' }}>
<Dialog.Close asChild>
<button className="Button green">儲存變更</button>
</Dialog.Close>
</div>
<Dialog.Close asChild>
<button className="IconButton" aria-label="Close">
<Cross2Icon />
</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
重點屬性:
Trigger: 打開 Dialog 的按鈕。Portal: 將內容渲染到 body 結尾(避免 z-index 問題)。Overlay: 背景遮罩。Content: 主要內容區域。asChild: Radix 的特殊設計。如果設為 true,它不會渲染自己的 DOM 節點,而是將功能與 props 傳遞給它的子元件。
搭配 Tailwind CSS
Radix 和 Tailwind 簡直是天作之合。因為 Radix 沒有樣式,你可以直接把 Tailwind classes 加上去。
import * as Switch from '@radix-ui/react-switch';
const Toggle = () => (
<div className="flex items-center">
<label className="text-white text-[15px] leading-none pr-[15px]" htmlFor="airplane-mode">
飛航模式
</label>
<Switch.Root
className="w-[42px] h-[25px] bg-blackA9 rounded-full relative shadow-[0_2px_10px] shadow-blackA7 focus:shadow-[0_0_0_2px] focus:shadow-black data-[state=checked]:bg-black outline-none cursor-default"
id="airplane-mode"
style={{ '-webkit-tap-highlight-color': 'rgba(0, 0, 0, 0)' }}
>
<Switch.Thumb className="block w-[21px] h-[21px] bg-white rounded-full shadow-[0_2px_2px] shadow-blackA7 transition-transform duration-100 translate-x-0.5 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div>
);
注意 Radix 提供了 data-state 屬性(如 data-state="checked"),你可以利用 Tailwind 的 data- 前綴來定義不同狀態的樣式:
data-[state=checked]:bg-black
動畫進場與離場 (Animation)
Radix 會自動在元件上根據目前狀態加上 data-state="open" 或 data-state="closed" 屬性。
我們可以利用 Tailwind 的 tailwindcss-animate 插件提供的 class,或是自定義 CSS Keyframes 來製作進出場動畫。
使用 Tailwind (需設定 keyframes):
/* 自定義 Keyframes */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.DialogContent[data-state='open'] {
animation: fadeIn 200ms ease-out;
}
.DialogContent[data-state='closed'] {
animation: fadeOut 200ms ease-in;
}
或者使用 Tailwind 語法:
<Dialog.Content
className="... data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
>
無障礙設計 (Accessibility Deep Dive)
Radix 最強大的地方在於它默默幫你處理了無數個無障礙細節:
- Focus Management (焦點管理):
- Focus Trap: 打開 Dialog 時,焦點會被「鎖」在對話框內,Tab 鍵出不去。
- Focus Restoration: 關閉 Dialog 後,焦點會自動回到原本觸發按鈕上。
- Screen Reader Support (螢幕閱讀器):
- 自動補上
aria-expanded,aria-controls,aria-labelledby等屬性。 - 使用
VisuallyHidden元件來提供僅螢幕閱讀器可見的描述。
- 自動補上
- Keyboard Navigation (鍵盤導航):
- Dropdown Menu 支援上下鍵選擇、Esc 關閉、Home/End 跳轉。
服務端渲染 (SSR)
如果你在 Next.js (App Router) 中使用 Radix UI,因為 Radix 依賴大量的 useState, useEffect 來處理互動邏輯,所以所有的 Radix 元件必須在 Client Component 中使用。
記得在檔案最上方加上:
'use client';
import * as Dialog from '@radix-ui/react-dialog';
// ...
常見元件庫 (shadcn/ui)
你也許聽過 shadcn/ui,它目前非常熱門。其實 shadcn/ui 不是一個元件庫(你不能 npm install 它),它是一套設計好的程式碼片段集合。
它的底層正是使用 Radix UI 處理互動,並使用 Tailwind CSS 處理樣式。這證明了 Radix + Tailwind 這種「Headless + Utility」模式是目前 React UI 開發的主流趨勢。
總結
使用 Radix UI,你可以獲得:
- 堅實的無障礙基礎 (WAI-ARIA compliance)。
- 完整的鍵盤導航支援。
- 絕對的樣式自由。
它是構建高品質 Design System 的最佳基石。