Live Preview
Spinner / Loading Component
Skill này hướng dẫn tạo component Spinner — circular loading indicator cho inline, button, section, và page loading states.
1. Mục tiêu (Objective)
Tạo component Spinner cho hiển thị loading state: inline, button, section, page. Dùng khi không biết content layout (nếu biết → dùng Skeleton).
2. AI Context & Intent (Ngữ cảnh cho AI)
Khi nào dùng Spinner?
- Button loading: Submit button đang xử lý
- Inline loading: "Saving..." next to text
- Section loading: Loading a widget/card
- Page loading: Initial page load
⚠️ Phân biệt Spinner vs Skeleton vs Progress Bar (QUAN TRỌNG)
| Tiêu chí | Spinner | Skeleton | Progress Bar |
|---|---|---|---|
| Bản chất | Circular animation | Content placeholder | Linear bar |
| Best for | Unknown layout | Known layout | Known progress % |
| Info | Just "loading" | Visual preview | Exact percentage |
| Shape | Circle | Mimics content | Horizontal bar |
| Ví dụ | Button loading | Page layout loading | File upload 45% |
Decision Tree cho AI
text
Cần show loading?
├─ Biết content layout sẽ hiện → Skeleton
├─ Biết progress % → Progress Bar
├─ Không biết layout/progress → Spinner
│ ├─ Inside button → Spinner (xs/sm, inverted nếu primary bg)
│ ├─ Inline text → Spinner (xs) + "Saving..."
│ ├─ Card/section → Spinner (md/lg) centered
│ └─ Full page → Spinner (xl) + optional backdrop
│
└─ Process steps → Stepper (read-only)
3. Anatomy
text
┌──┐
│╱ │ ← Circular arc (--primary)
│ │ Rotating animation
└──┘ stroke-width varies by size
Sizes:
· ○ ◎ ◉ ⊙
xs sm md lg xl
12px 20px 24px 32px 48px
Button loading:
┌──────────────────┐
│ ◎ Submitting │ ← Spinner replaces/accompanies text
└──────────────────┘ variant="inverted" on primary bg
4. Platform Mapping
| Platform | Component | Key Difference |
|---|---|---|
| Web | Spinner (CSS animation) | Circular ring, rotating |
| iOS | UIActivityIndicatorView | System spinner, 2 sizes |
| Android (M3) | CircularProgressIndicator | Indeterminate arc animation |
| Fluent | Spinner | Ring, 5 sizes, inverted variant |
| Carbon | InlineLoading / Loading | Inline + overlay variants |
Cross-platform: ALL platforms có spinner/loading indicator.
5. Sizes
xs (12px), sm (20px), md (24px), lg (32px), xl (48px)
6. Variants
| Variant | Use case |
|---|---|
default |
Primary color on light bg |
inverted |
White on dark/primary bg |
button |
Inside button, replaces label |
7. Token Mapping
📦 Atomic Mapping: Xem
ATOMIC-MAPPING.md→ mục spinner — Density Tier: comp.Color dùng
--primary, sizes dùng--icon-sm/md/lg, animation spin 0.75s linear infinite. Component token JSON files đã deprecated.
8. Props & API
typescript
interface SpinnerProps {
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
variant?: 'default' | 'inverted';
'aria-label'?: string; // Default: "Loading"
}
9. Accessibility (a11y)
role="status",aria-label="Loading".- Visually hidden text: "Loading..." for screen readers.
- Khi complete: remove spinner + announce "Content loaded" via live region.
10. Best Practices & Rules
- Button loading: Replace button text, keep button size, add
disabled. - Page loading: Center
xlspinner, optional backdrop overlay. - Inline loading:
xs/smnext to text ("Saving..."). - Skeleton preferred: Nếu biết layout, dùng Skeleton thay vì Spinner.
- Không Hardcode: Mọi giá trị từ Token.
11. Example Usage
jsx
{/* Button loading */}
<Button variant="primary" disabled>
<Spinner size="xs" variant="inverted" />
Submitting...
</Button>
{/* Section loading */}
<Card>
<div className="flex-center" style={{ minHeight: '200px' }}>
<Spinner size="lg" aria-label="Loading transactions" />
</div>
</Card>
{/* Inline loading */}
<div className="inline-flex items-center gap-2">
<Spinner size="xs" />
<span>Saving changes...</span>
</div>
{/* Full page loading */}
<div className="page-loader">
<Spinner size="xl" aria-label="Loading application" />
</div>