🔬 Phân tích UX: Vị trí cụm nút (Action Group Placement)
Ngày: 2026-03-12 | Phạm vi: Web + App | Mức độ: 🔴 Critical (UX consistency)
1. Vấn đề phát hiện
1.1 Web: Cụm nút lúc bên trái, lúc bên phải
Nguyên nhân gốc: Có 3 CSS class khác nhau cho footer/action group, mỗi class dùng alignment khác nhau:
| CSS Class | justify-content |
Alignment | Đang dùng bởi |
|---|---|---|---|
.slot-footer |
flex-end |
▶ Phải | Slot system (chuẩn) |
.slot-footer-clean |
flex-end |
▶ Phải | Slot system (no border) |
.slot-footer-between |
space-between |
◀▶ Hai bên | Slot system (variant) |
.drawer-preview-footer |
flex-end |
▶ Phải | Dialog/Modal/Drawer previews |
.card-footer |
(không set) | ◀ Trái (default) | Card previews |
⚠️
.card-footerKHÔNG cójustify-content→ mặc địnhflex-start(trái), trong khi toàn bộ overlay components (dialog, modal, drawer) đều canh phải (flex-end).
Kết quả: User nhìn Card thấy nút bên trái, mở Dialog/Modal thấy nút bên phải → mất consistency, vi phạm NNG H4 (Consistency & Standards).
1.2 App: Chưa có layout spec cho cụm nút
Hiện tại, DS chỉ có:
- Button pairing rules (§4.5) — variant ghép đôi (primary ↔ outline)
- Button size rules (BS-1 → BS-5) — quy tắc chọn size
- ❌ KHÔNG CÓ quy tắc về vị trí / alignment của cụm nút
- ❌ KHÔNG CÓ utility class
.action-grouphay.action-barcho app context
2. Phân tích hệ thống tham chiếu
2.1 So sánh Web vs App
| Yếu tố | Web Desktop | App Mobile |
|---|---|---|
| Chiều rộng viewport | Rộng (1024px+) → buttons nhỏ so với container → cần anchor (phải) | Hẹp (320-414px) → buttons gần full-width → center tự nhiên |
| Thao tác tay | Chuột → không giới hạn vùng | Ngón cái → vùng trung tâm thoải mái nhất (thumb zone) |
| Visual weight | Nhiều content → buttons cần "neo" vào góc phải | Ít content → center tạo cân bằng thị giác |
| Pattern phổ biến | shadcn/MD3 web → flex-end | iOS Alert, Bottom Sheet, Banking CTA → center |
2.2 Reference Systems
Material Design 3:
- Web Dialog/AlertDialog → trailing (phải)
- Mobile Bottom Sheet → center, full-width buttons
- Card actions → thường trailing
Apple HIG:
- Alert (iOS) → center (stacked full-width)
- Sheet (macOS) → trailing
- Navigation Bar → leading/trailing slots
shadcn/ui:
- Dialog / AlertDialog →
flex-end(phải) - Card →
flex(no justify — left default)
Banking apps (VCB, Momo, ZaloPay): CTA luôn center full-width.
3. Đề xuất: Hệ thống Action Group Placement
3.1 Nguyên tắc cốt lõi — Platform-Aware Alignment
Platform Quy tắc alignment Lý do Web (desktop) ▶ Trailing (flex-end) Z-pattern scan, Fitts's Law, MD3/shadcn consensus App (mobile) ▸ Center (full-width, centered) Thumb zone, visual balance, iOS/Android native pattern
Đây là sự khác biệt có chủ đích dựa trên platform context, không phải inconsistency.
3.2 Bảng quy tắc — AG Rules
| # | Rule | Web | App | Source |
|---|---|---|---|---|
| AG-1 | Web trailing — Cụm nút trên web desktop mặc định justify-content: flex-end |
✅ | — | MD3, shadcn, Z-pattern |
| AG-2 | App center — Cụm nút trên mobile app mặc định center, full-width | — | ✅ | HIG, Thumb Zone Law |
| AG-3 | Button order — Cancel/secondary trước (trái), Confirm/primary sau (phải) | ✅ | ✅ | MD3, shadcn |
| AG-4 | Gap chuẩn — Inline: gap: var(--spacing-2) (8px). Stacked: gap: var(--spacing-3) (12px) |
✅ | ✅ | 4px grid (S2) |
| AG-5 | Mobile CTA stacked — Mobile (≤768px), ≥2 buttons → stack vertical, full-width, CTA trên | ✅ | ✅ | HIG, BS-4 |
| AG-6 | Between variant — slot-footer-between khi cần metadata trái + action phải |
✅ | ✅ | MD3 Card pattern |
| AG-7 | Sticky action bar — CTA quan trọng (checkout, submit) → sticky bottom, safe-area padding | — | ✅ | iOS Safe Area |
3.3 CSS Implementation
css
/* ─── Action Group — Web: trailing, responsive stack ─── */
.action-group {
display: flex;
justify-content: flex-end;
align-items: center;
gap: var(--spacing-2);
flex-wrap: wrap;
}
/* Mobile: center, full-width stacked */
@media (max-width: 768px) {
.action-group--responsive {
flex-direction: column-reverse;
align-items: stretch;
}
.action-group--responsive > .btn {
width: 100%;
justify-content: center;
}
}
3.4 Fix List
| Fix | File | Chi tiết |
|---|---|---|
| 🔴 | components.css |
.card-footer thêm justify-content: flex-end; |
| 🟡 | components.css |
Thêm .action-group + responsive variant |
| 🟡 | button.md |
Thêm §4.8 — Action Group Placement Rules |
| 🟡 | UX skill domains-nav-layout-component.md |
Thêm §5.5 AG rules |
3.5 Sơ đồ visual
text
WEB — Overlay/Card: Trailing (flex-end)
┌────────────────────────────────┐
│ Title [✕] │
│ Description text... │
│ [Cancel] [Confirm] │ ← flex-end ✅
└────────────────────────────────┘
APP — Mobile: Center, stacked full-width
┌────────────────────────────────┐
│ Title │
│ Content... │
│ ┌──────────────────────────┐ │
│ │ Xác nhận │ │ ← full-width center ✅
│ └──────────────────────────┘ │
│ ┌──────────────────────────┐ │
│ │ Huỷ │ │ ← full-width center ✅
│ └──────────────────────────┘ │
└────────────────────────────────┘