前幾天完成了:
Day 11 → Input
Day 12 → Field
Day 13 → Textarea
今天開始處理另一類表單元件:
Checkbox
Radio
Switch
它們看起來都在做:
「選」或「不選」
但實際上三者代表的操作語意並不一樣。
Checkbox → 可以選,也可以不選;多個選項可以同時成立
Radio → 一組選項中只能選一個
Switch → 切換某個功能目前的開啟 / 關閉狀態
選錯元件,即使畫面看起來差不多,使用者理解到的意思卻可能完全不同。
今天就來建立 CUI 第一版 Selection Controls。
最常見的是:
☑ 我同意使用條款
或:
偏好的通知方式
☑ Email
☐ 簡訊
☑ App 通知
每一個選項都是獨立成立的。
因此可以:
Email ✓
簡訊 ✓
App ✓
三個同時被選取。
這就是 Checkbox 最重要的語意。
先加入元件:
npx shadcn@latest add checkbox
接著延續 CUI 的 Component Contract:
<Checkbox
data-cui-slot="checkbox"
/>
並確認幾個基本狀態:
Unchecked
Checked
Focus
Disabled
Invalid
使用:
<Checkbox id="terms" />
單獨放:
<Checkbox />
視覺上只會看到一個方框。
使用者根本不知道:
我要勾什麼?
因此最基本的結構應該是:
<div className="flex items-center gap-2">
<Checkbox id="terms" />
<label htmlFor="terms">
我同意使用條款
</label>
</div>
點擊:
我同意使用條款
也應該可以切換 Checkbox。
這跟之前 Input 的:
<label htmlFor="email">
其實是同一個概念。
接著是 Radio。
例如:
付款方式
◉ 信用卡
○ ATM
○ 超商付款
和 Checkbox 最大的差別:
Checkbox
☑ A
☑ B
☑ C
Radio
◉ A
○ B
○ C
Radio 是「從一組選項中選一個」。
因此它通常不應該單獨存在,而是:
RadioGroup
├─ Radio A
├─ Radio B
└─ Radio C
npx shadcn@latest add radio-group
使用:
<RadioGroup defaultValue="card">
<div className="flex items-center gap-2">
<RadioGroupItem
value="card"
id="card"
/>
<label htmlFor="card">
信用卡
</label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem
value="atm"
id="atm"
/>
<label htmlFor="atm">
ATM
</label>
</div>
</RadioGroup>
選擇 ATM 後,信用卡就會取消。
這是 RadioGroup 本身應該提供的行為,而不是自己用 JavaScript 一個一個取消。
每個 Radio 有自己的 Label:
○ 信用卡
○ ATM
○ 超商付款
但還少了一個問題:
這三個選項到底在選什麼?
所以完整結構應該像:
付款方式
○ 信用卡
○ ATM
○ 超商付款
「付款方式」描述的是整組 Radio,而不是其中某一顆。
使用原生 HTML 時,可以想到:
<fieldset>
<legend>付款方式</legend>
...
</fieldset>
這也是很重要的原則:
不只每一個 Control 要有名稱,一整組相關 Control 也需要有名稱。
之後 CUI 在做更完整的 Radio Field 時,也會把 Group Label 納入考量。
Switch 看起來也只有兩種狀態:
OFF / ON
但它和 Checkbox 的語意不同。
例如:
深色模式 [ ON ]
Switch 表達的是:
「這個功能現在開著還是關著?」
而 Checkbox 比較像:
「我要不要選擇這個項目?」
npx shadcn@latest add switch
例如:
<div className="flex items-center gap-2">
<Switch id="notifications" />
<label htmlFor="notifications">
開啟通知
</label>
</div>
同樣不要只有:
<Switch />
而沒有任何可理解的名稱。
這兩個最容易混淆。
可以用一個簡單問題判斷:
我是在「選擇一件事情」,還是在「切換一個目前狀態」?
例如:
☑ 我同意服務條款
比較適合 Checkbox。
因為這是表單中的一個選擇。
而:
Email 通知 [ ON ]
比較適合 Switch。
因為這是在控制:
Email Notification = Enabled
還有一個 UX 上很常見的差異。
Checkbox 常常出現在表單:
☑ 接收活動通知
[ 儲存設定 ]
使用者最後按:
儲存設定
才真正送出。
Switch 則常讓人預期:
通知 [ OFF ]
↓
click
↓
通知 [ ON ]
切換後狀態立即改變。
所以如果畫面用了 Switch,卻還要求:
切 Switch
↓
按儲存
↓
才生效
就要特別確認這是不是使用者預期的互動方式。
這不完全是程式問題,而是 Component 的語意會影響 UX 預期。
例如 Checkbox:
Unchecked → 灰色
Checked → 藍色
如果唯一差異只有:
灰 → 藍
對部分使用者來說可能不夠明確。
Checked State 應該還有清楚的形狀提示:
☐
☑
Radio:
○
◉
Switch:
○────
────●
也就是:
Color 可以強化狀態,但不應該是唯一的狀態資訊。
Selection Controls 一樣必須可以使用鍵盤操作。
延續 CUI 前面建立的 Focus Style:
"focus-visible:border-ring"
"focus-visible:ring-3"
"focus-visible:ring-ring/50"
測試時不要只用滑鼠。
直接:
Tab
Space
Arrow Keys
實際操作看看。
Checkbox 通常可以:
Tab → Focus
Space → Toggle
Switch:
Tab → Focus
Space → Toggle
Radio Group 則需要特別注意:
Tab
→ 進入 Radio Group
Arrow Keys
→ 在選項間移動
這些 Keyboard Interaction 如果底層元件已經正確提供,我們就不需要自己重新實作。
三種 Control 都可能 Disabled:
<Checkbox disabled />
<RadioGroupItem
value="card"
disabled
/>
<Switch disabled />
視覺上可以降低強度,但:
Disabled
≠
只是變成灰色
真正重要的是它不能再被操作。
因此仍然優先使用元件提供的原生 / Primitive Disabled API,而不是只寫:
className="opacity-50"
做到今天,我們可以繼續建立:
data-cui-slot="checkbox"
data-cui-slot="radio-group"
data-cui-slot="radio-group-item"
data-cui-slot="switch"
未來 CUI 的 DOM Contract 就會慢慢形成:
button
input
textarea
checkbox
radio-group
radio-group-item
switch
React 版本與未來的 CDN / Legacy 版本,都可以沿用同樣的命名概念。
最後把三種元件放在一起:
Selection Controls
Checkbox
☐ 我同意使用條款
☑ 接收 Email 通知
Radio
付款方式
◉ 信用卡
○ ATM
○ 超商付款
Switch
深色模式 [ OFF ]
Email 通知 [ ON ]
除了 Default,也可以放:
Checked
Unchecked
Disabled
Focus
幾個代表案例。
今天特別值得做一輪:
1. 不碰滑鼠
2. 按 Tab 移動
3. 找到 Checkbox
4. Space 切換
5. Tab 到 Radio Group
6. 使用方向鍵切換 Radio
7. Tab 到 Switch
8. Space 切換
如果完全不用滑鼠也能完成操作,才代表這些 Control 的基本 Keyboard Interaction 是成立的。
今天加入三種 Selection Controls:
✓ Checkbox
✓ Radio Group
✓ Switch
✓ 每個 Control 都有可理解的 Label
✓ Radio Group 本身也需要 Group Label
✓ Checked State 不只靠顏色
✓ Focus State
✓ Disabled State
✓ Keyboard Interaction
✓ Checkbox → 獨立選擇
✓ Radio → 一組選一個
✓ Switch → 開啟 / 關閉目前狀態
三種元件乍看之下都只有:
Yes / No
On / Off
Selected / Unselected
但真正決定該使用哪一個的,不是它們「長什麼樣子」,而是:
這個操作對使用者來說代表什麼?
做到 Day 14,CUI 的 Form Family 也越來越完整:
Form
│
├─ Field
│ ├─ FieldLabel
│ ├─ FieldDescription
│ └─ FieldError
│
├─ Input
├─ Textarea
│
└─ Selection Controls
├─ Checkbox
├─ Radio Group
└─ Switch
前面幾天主要處理「輸入」與「選擇」。
下一篇 Day 15,可以開始進入更有互動性的元件:Select——為什麼原生 <select> 很好用,而自訂 Select 又會突然讓無障礙難度上升?