Live Preview
Confirm Action
Are you sure you want to delete this item? This action cannot be undone.
Modal Component Generation Skill
Skill này hướng dẫn bạn (AI Agent) tạo component Modal — overlay popup hiển thị nội dung phức tạp (forms, detail views, editors).
1. Mục tiêu (Objective)
Tạo component Modal hoàn chỉnh với focus trap, backdrop, animation, keyboard navigation, và nhiều variants. Tích hợp 100% Design Tokens.
2. AI Context & Intent (Ngữ cảnh cho AI)
Khi nào dùng Modal?
- Form phức tạp: Tạo nội dung mới, chỉnh sửa nhiều fields
- Chi tiết item: Xem chi tiết mà không rời trang
- Content editor: Rich text editor, image cropper
- Onboarding step: Welcome flow, tutorial
- Preview: Xem trước file, ảnh, document
Đặc điểm cốt lõi
- Bản chất: Content display — hiển thị rich information
- Content: Forms, tables, detail views, editors
- Scroll: ✅ Body scrollable
- Size: 384-1024px (web center overlay), 100% (app bottom sheet)
- Backdrop: ✅ Click dismiss
Decision Tree cho AI
text
Cần hiển thị rich content overlay?
├─ Form (2+ fields) → Modal (default)
├─ Nội dung chi tiết / preview → Modal (default)
├─ Content editor / complex UI → Modal (lg/xl)
├─ Fullscreen experience (mobile) → Modal (fullscreen)
└─ Navigation / filters / long form → Drawer (sheet from edge)
3. Ngữ nghĩa & Phân loại (Semantics)
3.1. Variants
| Variant | Mô tả | Khi nào dùng |
|---|---|---|
default |
Center overlay, content tự do | Form, detail view, editor |
fullscreen |
Toàn màn hình (mobile) | Complex form, preview |
drawer |
Slide từ cạnh (right/bottom) | Navigation, filters |
3.2. Sizes (variant = default)
| Size | Max width | Use case |
|---|---|---|
sm |
400px | Simple confirm, notification |
md |
500px | Standard form |
lg |
640px | Detail view |
xl |
800px | Complex content |
fullscreen |
100vw × 100vh | Mobile, preview |
3.3. Sub-components
| Part | Mô tả |
|---|---|
| Modal.Header | Title + close button (optional description) |
| Modal.Body | Scrollable content area |
| Modal.Footer | Action buttons (right-aligned) |
3.4. Slot Map (Figma ↔ Code)
📎 Source:
slot-manifest.json→modal· Layer: surface
| Figma Slot | data-slot |
CSS Class | Required | Accepts |
|---|---|---|---|---|
| Root | modal |
.surface-modal |
✅ | — |
| Header | modal-header |
.slot-header |
✅ | title-group |
| Title | modal-title |
— | ✅ | text |
| Description | modal-description |
— | ❌ | text |
| Close | modal-close |
— | ❌ | close-button |
| Body | modal-body |
.slot-body · .slot-body-flush |
❌ | input-group, list, table, form, rich-editor, * |
| Footer | modal-footer |
.slot-footer |
❌ | button-group |
html
<!-- HTML skeleton with data-slot -->
<div class="surface-modal" data-slot="modal" role="dialog" aria-modal="true">
<div class="slot-header" data-slot="modal-header">
<h2 data-slot="modal-title">Title</h2>
<button data-slot="modal-close" aria-label="Close">✕</button>
</div>
<div class="slot-body" data-slot="modal-body">
<!-- scrollable content -->
</div>
<div class="slot-footer" data-slot="modal-footer">
<button class="btn btn--outline">Cancel</button>
<button class="btn btn--primary">Save</button>
</div>
</div>
4. Token Mapping
📦 Token values: Xem
ATOMIC-MAPPING.md— single source of truth cho tất cả actual token values.
| Token | Type | Value | Mô tả |
|---|---|---|---|
max-width.sm |
number | {max-width.sm} |
Small modal — confirmations, simple alerts (384px) |
max-width.md |
number | {max-width.lg} |
Medium modal — forms, detail views (512px) |
max-width.lg |
number | {max-width.3xl} |
Large modal — complex content, tables (768px) |
max-width.xl |
number | {max-width.5xl} |
Extra large modal — full editors, dashboards (1024px) |
max-width.full |
string | 100% |
Full-width modal — mobile fullscreen, immersive flows |
padding |
number | {module.padding-large} |
Internal padding of modal content area |
padding-header |
number | {module.padding-default} |
Padding for modal header section |
padding-footer |
number | {module.padding-default} |
Padding for modal footer/action bar |
radius |
number | {module.radius-large} |
Corner radius of modal container |
backdrop-blur |
number | {blur.lg} |
Backdrop blur intensity behind modal |
color.backdrop |
color | {base.background-overlay} |
Semi-transparent backdrop |
color.surface |
color | {base.surface} |
Modal background |
color.surface-foreground |
color | {base.surface-foreground} |
Modal text color |
color.border |
color | {base.card-border} |
Modal border (optional) |
color.footer-border |
color | {base.border} |
Footer top separator |
color.close-button |
color | {base.muted-foreground} |
Close X icon |
color.close-hover |
color | {state-layer.hover} |
Close button hover |
color.title |
color | {base.foreground} |
Title text |
color.description |
color | {base.muted-foreground} |
Description text |
color.focus-ring |
color | {focus.ring-color} |
Focus ring color — ref shared/focus |
color.disabled-bg |
color | {base.muted} |
Disabled state background |
color.disabled-fg |
color | {base.muted-foreground} |
Disabled state foreground |
elevation |
shadow | {elevation.level-4} |
Shadow elevation for modal — level 4 (high) |
z-index |
number | {z-index.modal} |
Stacking order for modal layer |
header.font-size |
number | {font.size.lg} |
Modal title font size — heading-4 (18px) |
header.font-weight |
number | {font.weight.semibold} |
Modal title font weight |
close-button-size |
number | {min-width.8} |
Size of the close button in modal header |
gap |
number | {spacing.4} |
Vertical gap between modal content sections |
transition-enter |
string | {duration.slow-1} {easing.emphasized-decelerate} |
Modal entrance animation timing |
transition-exit |
string | {duration.normal-1} {easing.emphasized-accelerate} |
Modal exit animation timing |
5. Props & API
typescript
interface ModalProps {
/** Mở/đóng */
open: boolean;
/** Callback khi close (Escape, backdrop click, X button) */
onClose: () => void;
/** Variant */
variant?: 'default' | 'fullscreen' | 'drawer';
/** Drawer position */
drawerPosition?: 'right' | 'bottom' | 'left';
/** Size (variant=default) */
size?: 'sm' | 'md' | 'lg' | 'xl';
/** Có thể close khi click backdrop */
closeOnBackdrop?: boolean; // default: true (false cho alert)
/** Có thể close khi nhấn Escape */
closeOnEscape?: boolean; // default: true
/** Hiện close button (X) trong header */
showCloseButton?: boolean; // default: true
children: ReactNode;
}
interface ModalHeaderProps {
/** Title */
title: string;
/** Description */
description?: string;
}
interface ModalFooterProps {
children: ReactNode; // Buttons
}
6. Accessibility (a11y)
- Role:
role="dialog"+aria-modal="true". - Label:
aria-labelledbytrỏ đến title,aria-describedbytrỏ đến description. - Focus trap: Focus PHẢI bị trap bên trong modal — Tab cycle qua focusable elements.
- Initial focus: Focus vào element đầu tiên (hoặc primary action button).
- Return focus: Khi close, trả focus về trigger element ban đầu.
- Escape: Nhấn
Escapeclose modal. - Scroll lock: Body scroll bị khoá khi modal mở.
- Tab order: Focusable elements trong modal giữ đúng tab order.
7. Best Practices & Rules
- Không lạm dụng: Modal interrupts workflow — chỉ dùng khi thật sự cần user attention.
- Portal: Render modal qua Portal vào
<body>— tránh z-index/overflow issues. - Animation: Fade in backdrop + scale up modal (200ms ease). Reverse khi close.
- Nested modals: TRÁNH nest modal trong modal — redesign UX nếu cần.
- Mobile: Trên mobile, dùng
fullscreenhoặcdrawer(bottom sheet) thay vì center modal nhỏ. - Không Hardcode: Mọi giá trị từ Token, đặc biệt backdrop overlay + surface colors.
- Icon: Close button (
x) CHỈ dùng Lucide icon từassets/icons/. CẤM dùng text emoji. Xemicon.mdmục 10. - Button size (BS-1, BS-3): Modal.Footer buttons PHẢI cùng size (BS-3). Standard form →
md. Mobile fullscreen →btn-lg.btn--block(BS-4). KHÔNG dùngbtn-lgcho mọi modal. Xembutton.md§4.7.
8. Example Usage
jsx
{/* Form modal */}
<Modal open={isOpen} onClose={close} size="md">
<Modal.Header title="Thêm thẻ mới" />
<Modal.Body>
<Input label="Số thẻ" placeholder="0000 0000 0000 0000" />
<Input label="Tên chủ thẻ" />
</Modal.Body>
<Modal.Footer>
<Button variant="outline" onClick={close}>Huỷ</Button>
<Button variant="primary" onClick={handleSave}>Lưu</Button>
</Modal.Footer>
</Modal>
{/* Bottom sheet (mobile) */}
<Modal open={isOpen} onClose={close} variant="drawer" drawerPosition="bottom">
<Modal.Header title="Chọn phương thức" />
<Modal.Body>{/* options */}</Modal.Body>
</Modal>