UX State Showcase

Bộ component cấp KIT (brand-agnostic) cho mọi trạng thái user gặp khi tương tác data: loading, empty, error, toast, focus. ARIA-compliant, VN voice mặc định.

📄 Doc tham chiếu

1 · Loading primitives

4 atoms — skeleton (cấu trúc trống), spinner (action ngắn), progress (xác định, >10s), dots (subtle inline).

Skeleton

atoms/loading-skeleton.html
Khi nào dùng: Task tải >1s, cần giữ layout ổn định, tránh layout shift khi data về. Parent container BẮT BUỘC có role="status" + aria-label.
<div role="status" aria-label="Đang tải…">
  <span class="skeleton skeleton-line skeleton-line--title"></span>
  <span class="skeleton skeleton-line"></span>
  <span class="skeleton skeleton-line skeleton-line--short"></span>
</div>

Spinner

atoms/loading-spinner.html
Khi nào dùng: Action ngắn 200ms–2s. Cho task >1s nên ưu tiên skeleton (giảm cognitive load — NN/g). Trong button: parent dùng aria-busy="true" + spinner aria-hidden.
<span class="spinner" role="status" aria-label="Đang tải"></span>

Progress · linear + circular

atoms/loading-progress.html
Tải file lên32%
Đang xử lý
25%
67%
92%
Khi nào dùng: Task >10s mà đo được % (NN/g threshold) — upload, batch, wizard. Indeterminate khi không đo được, nhưng tránh nếu >30s. Per WAI-ARIA 1.2 indeterminate omit aria-valuenow.
<div class="progress" role="progressbar"
     aria-valuenow="32" aria-valuemin="0" aria-valuemax="100"
     aria-label="Tải file lên">
  <div class="progress__bar" style="width: 32%"></div>
</div>

Dots · subtle inline

atoms/loading-dots.html
AI đang trả lời
Khi nào dùng: Subtle inline — typing indicator, filter recompute, chip status. Visual weight nhẹ hơn spinner.
<span class="dots" role="status" aria-label="Đang tải">
  <span class="dots__dot"></span>
  <span class="dots__dot"></span>
  <span class="dots__dot"></span>
</span>

2 · Empty state

4 biến thể tuỳ ngữ cảnh "tại sao trống". Icon + title + body + CTA phù hợp.

No data · No results · First use · Offline

molecules/empty-state.html

Chưa có dữ liệu nào

Bắt đầu bằng mục đầu tiên.

Không tìm thấy kết quả

Thử thay đổi từ khoá hoặc bỏ bộ lọc.

Chào mừng đến Cowork

Thiết lập 3 bước để bắt đầu.

Mất kết nối mạng

Kiểm tra Internet rồi thử lại.

Decision: 0 row + chưa filter → no-data. 0 row + đang filter → no-results. Account mới → first-use. navigator.onLine === falseoffline.

3 · Error state

3 scope: inline form field · block (1 widget fail) · full-page (route fatal).

Inline form error

molecules/error-state.html
Pattern: Input dùng aria-invalid="true" + aria-describedby trỏ tới <p role="alert">. SR sẽ đọc lỗi khi focus vào input.

Block error (1 widget fail)

molecules/error-state.html
Khi nào: 1 widget/section fail nhưng phần còn lại của page vẫn dùng được. role="alert" để SR ngắt và đọc.

4 · Toast

Phản hồi tạm thời sau action. Auto-dismiss 5s, hover pause, max 3 visible, slide-in motion.

4 severity

organisms/toast.html
aria-live mapping: info/success → role="status" polite. warn/error → role="alert" assertive. Mỗi toast có aria-atomic="true" để SR đọc đủ title + body.
Toast.show({
  severity: 'success',    // info | success | warn | error
  title:    'Đã lưu',
  body:     'Báo cáo đã cập nhật.',
  timeout:  5000          // ms; 0 = giữ vĩnh viễn
});

5 · Focus management

JS utilities lo focus trap (modal/drawer), Escape broadcast, skip-link auto-inject, keyboard shortcut hint badges.

Focus trap demo (modal)

_assets/focus-trap.js

Bấm Tab/Shift+Tab — focus vòng quanh trong modal. Nhấn Esc để đóng.

<script src="../_assets/focus-trap.js"></script>
<script>
  const trap = UXKit.createFocusTrap(document.getElementById('my-modal'));
  trap.activate();   // khi mở
  trap.deactivate(); // khi đóng — focus trả về previous element
</script>

Keyboard shortcuts

_assets/keyboard.js

Thử: nhấn Esc để dispatch event ux:escape (mọi toast đang mở sẽ đóng). Bấm Ctrl+K để demo shortcut registry.

Skip-link auto-inject: nhấn Tab lần đầu trên trang → thấy "Bỏ qua tới nội dung" góc trên-trái.

<script src="../_assets/keyboard.js"></script>
<script>
  // Auto behavior: Escape → dispatch CustomEvent 'ux:escape'
  document.addEventListener('ux:escape', () => closeOverlay());

  // Shortcut registry (optional)
  UXKit.kbd.shortcut('Ctrl+K', () => openCommandPalette());
</script>