Stacking Design — Hướng Dẫn Build Screen Từ Zero
Dành cho: Developer mới tham gia dự án Mục tiêu: Hiểu cách build bất kỳ screen nào bằng phương pháp "xếp tầng" 4 lớp + Z-axis + Elevation Naming Thời gian đọc: ~20 phút
Mở Đầu: Tại Sao Cần Stacking Design?
Hãy tưởng tượng bạn đang xây một toà nhà:
🏢 Toà nhà = Screen
├── 🧱 Móng + Khung (Page Shell) → Page tier
├── 🚪 Các tầng/phòng (Sections) → Section tier
├── 📦 Đồ nội thất (Cards, Panels) → Group tier
└── 🔧 Chi tiết (Buttons, Inputs) → Comp tier
Thay vì "vẽ" screen rồi đoán spacing/padding, Stacking Design cho bạn một quy trình rõ ràng:
- Vẽ Elevation Map → xác định mỗi element thuộc layer nào (Ground / Card / Surface)
- Chọn shell → toàn bộ page structure
- Chia vùng → các khu vực nội dung (sections)
- Đặt container → cards, panels (modules)
- Fill chi tiết → buttons, inputs (components)
- Thêm overlay → toast, modal, tooltip (Z-axis)
Mỗi bước đều có token sẵn — bạn không cần đoán spacing!
1. Khái Niệm Core: 2 Trục Xếp Tầng
Stacking Design xếp chồng theo 2 trục:
Trục XY — Nesting Hierarchy (Lồng nhau)
Các element lồng vào nhau theo 4 cấp, từ ngoài vào trong:
┌─────────────────────────────────────────────┐
│ PAGE │
│ ┌────────────────────────────────────────┐ │
│ │ SECTION │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ GROUP (Card/Panel) │ │ │
│ │ │ ┌────────────┐ ┌────────────┐ │ │ │
│ │ │ │ COMP │ │ COMP │ │ │ │
│ │ │ │ (btn/input)│ │ (btn/input)│ │ │ │
│ │ │ └────────────┘ └────────────┘ │ │ │
│ │ └──────────────────────────────────┘ │ │
│ └────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
Trục Z — Elevation Hierarchy (Nổi lên)
Các element "bay lên" khỏi nội dung theo z-index:
Z-axis (elevation)
│
│ ┌─ Toast/Tooltip ─────────────────┐ z-1600+
│ │ │
│ ├─ Modal/Dialog ──────────────────┤ z-1400
│ │ │
│ ├─ Drawer/Popover ────────────────┤ z-1300
│ │ │
│ ├─ Dropdown ──────────────────────┤ z-1000
│ │ │
│ ├─ Content (page > section > ...) │ z-0
│ │ │
└──┴──────────────────────────────────┘
Tóm lại: XY = element nào chứa element nào. Z = element nào nổi trên element nào.
2. Elevation Naming Convention — Bước Đầu Tiên
⚠️ Đây là thay đổi quan trọng nhất so với phiên bản cũ. Mọi CSS class phải theo convention này.
Trước khi viết bất kỳ dòng code nào, bạn cần vẽ Elevation Map — xác định mỗi UI element thuộc layer nào.
Class Syntax
.[layer]-[component]__[element]--[modifier]
3 Layer Chính
| Layer | Prefix | Background | Foreground | Border | Shadow |
|---|---|---|---|---|---|
| Ground | ground- |
--background hoặc --canvas |
--foreground |
--border |
None |
| Card | card- |
--card |
--card-foreground |
--card-border |
--shadow-sm |
| Surface | surface- |
--surface |
--surface-foreground |
--border |
--shadow-lg |
Ví Dụ Elevation Map
Dashboard Screen
├── ground-app → body/background (Ground layer)
│ ├── surface-nav → top navbar (Surface layer)
│ ├── surface-sidebar → left sidebar (Surface layer)
│ └── ground-content → main scrollable area (Ground layer)
│ ├── card-stat → stat card ×4 (Card layer)
│ ├── card-chart → chart container (Card layer)
│ └── card-table → data table (Card layer)
Rules
- Token constraint: KHÔNG dùng hardcoded RGB/HEX cho
background,color,border-color. PHẢI dùng token theo layer contract - Item-level (Comp tier): Buttons, badges, avatars KHÔNG dùng layer prefix — chúng inherit từ parent layer
- Semantic naming: Tên class theo UI role, KHÔNG theo business data
- ✅
.card-account,.card-product - ❌
.balance-card,.loan-section
- ✅
3. Chi Tiết 4 Lớp XY
🔷 Lớp 1: Page — Khung ngoài cùng
Vai trò: Outer structure của cả trang — chứa navbar, sidebar, main content area.
Khi nào là Page? Khi element là container lớn nhất chứa toàn bộ sections.
<body class="ground-app">
<header class="surface-nav"><!-- Navbar --></header>
<div class="ground-app__layout">
<aside class="surface-sidebar"><!-- Sidebar --></aside>
<main class="ground-content">
<!-- Content goes here -->
</main>
</div>
</body>
Tokens sử dụng:
| Property | Token |
|---|---|
| Padding | --page-padding-large, --page-padding-default, --page-padding-small |
| Margin | --page-margin-large, --page-margin-default, --page-margin-small |
| Stack gap | --page-stack-gap-xlarge, --page-stack-gap-large, --page-stack-gap-default, --page-stack-gap-small |
| Inline gap | --page-inline-gap-default, --page-inline-gap-small |
| Grid gap | --page-grid-row-gap, --page-grid-column-gap |
| Radius | --page-radius-default, --page-radius-none |
🟢 Lớp 2: Section — Vùng nội dung
Vai trò: Nhóm nội dung theo logic — mỗi section là một "vùng" có ý nghĩa riêng (profile info, stats, settings).
Khi nào là Section? Khi element chứa nhiều card/module siblings.
<main class="ground-content">
<!-- Section 1: Stats -->
<section class="ground-content__stats">
<article class="card-stat">...</article>
<article class="card-stat">...</article>
<article class="card-stat">...</article>
<article class="card-stat">...</article>
</section>
<!-- Section 2: Chart -->
<section class="ground-content__chart">
<article class="card-chart">...</article>
</section>
<!-- Section 3: Table -->
<section class="ground-content__table">
<article class="card-table">...</article>
</section>
</main>
CSS tương ứng:
.ground-content {
display: flex;
flex-direction: column;
gap: var(--page-stack-gap-default);
padding: var(--page-padding-default);
}
.ground-content__stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--section-grid-row-gap) var(--section-grid-column-gap);
}
.ground-content__chart,
.ground-content__table {
display: flex;
flex-direction: column;
gap: var(--section-stack-gap-default);
}
Tokens sử dụng:
| Property | Token |
|---|---|
| Padding | --section-padding-large, --section-padding-default, --section-padding-small |
| Margin | --section-margin-large, --section-margin-default |
| Stack gap | --section-stack-gap-large, --section-stack-gap-default, --section-stack-gap-small |
| Inline gap | --section-inline-gap-default, --section-inline-gap-small |
| Grid gap | --section-grid-row-gap, --section-grid-column-gap |
| Radius | --section-radius-default, --section-radius-small |
🟡 Lớp 3: Group — Card, Panel, Dialog
Vai trò: Container có visual boundary (background, border, shadow) — cards, panels, input groups.
Khi nào là Group? Khi element có background/border/shadow riêng.
<section class="ground-content__stats">
<!-- Mỗi card là 1 group -->
<article class="card-stat">
<span class="card-stat__label">Revenue</span>
<span class="card-stat__value">$12,450</span>
<span class="card-stat__trend">+12% from last month</span>
</article>
<article class="card-stat">
<span class="card-stat__label">Users</span>
<span class="card-stat__value">1,234</span>
<span class="card-stat__trend">+5% from last month</span>
</article>
</section>
CSS tương ứng:
.card-stat {
background: var(--card);
color: var(--card-foreground);
border: 1px solid var(--card-border);
border-radius: var(--group-radius-default);
padding: var(--group-padding-default);
display: flex;
flex-direction: column;
gap: var(--group-stack-gap-default);
}
Tokens sử dụng:
| Property | Token |
|---|---|
| Padding | --group-padding-large, --group-padding-default, --group-padding-small |
| Margin | --group-margin-large, --group-margin-default |
| Stack gap | --group-stack-gap-large, --group-stack-gap-default, --group-stack-gap-small |
| Inline gap | --group-inline-gap-default, --group-inline-gap-small |
| Grid gap | --group-grid-row-gap, --group-grid-column-gap |
| Radius | --group-radius-default, --group-radius-small |
🔴 Lớp 4: Comp — Button, Input, Badge
Vai trò: Element tương tác cuối cùng — leaf node, không chứa children phức tạp.
Khi nào là Comp? Khi element là button, input, badge, checkbox, tag, v.v.
Quan trọng: Comp tier KHÔNG dùng layer prefix (ground/card/surface). Chúng inherit styles từ parent layer.
<article class="card-form">
<h3>Create New Project</h3>
<div class="card-form__fields">
<input type="text" class="input" placeholder="Project name" />
<textarea class="textarea" placeholder="Description"></textarea>
</div>
<div class="card-form__actions">
<button class="btn btn--primary">Create</button>
<button class="btn btn--secondary">Cancel</button>
</div>
</article>
CSS tương ứng:
.card-form__fields {
display: flex;
flex-direction: column;
gap: var(--comp-stack-gap-default);
}
.card-form__actions {
display: flex;
flex-direction: row;
gap: var(--comp-inline-gap-default);
align-items: center;
}
Tokens sử dụng:
| Property | Token |
|---|---|
| Padding | --comp-padding-large, --comp-padding-default, --comp-padding-small, --comp-padding-xsmall |
| Margin | --comp-margin-default |
| Stack gap | --comp-stack-gap-default, --comp-stack-gap-small |
| Inline gap | --comp-inline-gap-default, --comp-inline-gap-small |
| Radius | --comp-radius, --comp-radius-small, --comp-radius-capsule |
| Height | --comp-height-large, --comp-height-default, --comp-height-small |
4. Chọn Tier Đúng — Decision Tree
Khi bạn gặp bất kỳ element nào, hãy trả lời theo flowchart:
Q1: Element có background/border/shadow riêng?
├── CÓ → Group tier (card, panel, input-group)
│ → Prefix = card- hoặc surface-
└── KHÔNG → Q2: Element chứa children loại gì?
├── Sibling GROUPS (cards, panels) → Section tier
├── Sibling ITEMS (buttons, inputs) → Group tier
├── Sibling SECTIONS → Page tier
└── Text/icon (leaf content) → Comp tier (NO prefix)
Ví Dụ Thực Tế
| Element | Q1: Có border/bg? | Q2: Children? | → Tier | CSS Class |
|---|---|---|---|---|
| Page wrapper | ❌ | Chứa sections | Page | ground-app |
| "User Profile" area | ❌ | Chứa cards | Section | ground-content__profile |
| Revenue card | ✅ có shadow | — | Group | card-stat |
| Login button | ❌ | Text/icon | Comp | btn btn--primary |
| Form container | ✅ có border | — | Group | card-form |
| Stats row | ❌ | Chứa cards | Section | ground-content__stats |
| Top navbar | ✅ elevated | — | Group | surface-nav |
5. Quy Tắc Xếp Tầng (Stacking Rules)
Nesting hierarchy tuân theo quy tắc "ngoài lớn, trong nhỏ":
| Rule | Mô tả | Ý nghĩa |
|---|---|---|
| Radius nesting | Outer ≥ Inner radius | page.radius ≥ section.radius ≥ group.radius ≥ comp.radius |
| Padding nesting | Outer ≥ Inner padding | page.padding > section.padding > group.padding > comp.padding |
| Gap scaling | Outer > Inner gap | page.gap > section.gap > group.gap > comp.gap |
| Color depth | Deeper = subtler colors | background → card → card-subtle → surface-subtle |
| Elevation nesting | Never nest same-layer | Không bao giờ card-* bên trong card-* |
Tại sao? Vì mắt người nhận biết "quan trọng" qua spacing — element ngoài cùng cần breathing room lớn hơn, element bên trong cần tiết kiệm không gian.
6. Slot Anatomy — Header / Body / Footer
Mỗi tier container có thể chia thành 3 slot (composition axis thứ 2):
┌─────────────────────────────────────┐
│ TIER (page/section/group/comp) │ ← Nesting axis
│ ┌─ slot-header ─────────────────┐ │
│ │ title | actions | meta │ │ ← Slot axis
│ ├───────────────────────────────┤ │
│ │ slot-body │ │
│ │ (main content, flex:1) │ │
│ ├───────────────────────────────┤ │
│ │ slot-footer │ │
│ │ actions | summary │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
Ví Dụ: Card Có Đầy Đủ Slots
<div class="card-transactions slotted">
<div class="slot-header">
<h3>Recent Transactions</h3>
<button class="btn btn--outline">Export</button>
</div>
<div class="slot-body-flush">
<table>...</table>
</div>
<div class="slot-footer-between">
<span>Showing 1-10 of 50</span>
<div>pagination buttons</div>
</div>
</div>
Slot Variants
| Class | Effect |
|---|---|
.slot-header |
Border-bottom, flex between |
.slot-header-clean |
No border, tighter coupling with body |
.slot-body |
Standard padding, flex: 1, overflow: auto |
.slot-body-flush |
No padding (cho tables, full-bleed content) |
.slot-body-compact |
Reduced vertical padding |
.slot-footer |
Border-top, flex end |
.slot-footer-clean |
No border |
.slot-footer-between |
Border-top, space-between |
Quan trọng: Slot classes không có layer prefix. Parent container mang prefix, slots inherit context:
css.card-account .slot-header { /* inherits card context */ } .surface-nav .slot-body { /* inherits surface context */ }
7. Density System — Tự Động Scale
Prisma sử dụng 2 density profiles: web (default) và mobile.
Cách Hoạt Động
| Profile | --spacing |
Trigger |
|---|---|---|
| web | 4px |
Default, viewport > 1023px |
| mobile | 4px |
@media (width <= 1023px) hoặc data-density="mobile" |
Mọi tier token đều dùng calc() dựa trên biến --spacing:
/* Ví dụ: section padding tự scale theo density */
--section-padding-default: calc(var(--spacing) * 4);
--group-padding-default: calc(var(--spacing) * 4);
--comp-padding-default: calc(var(--spacing) * 4);
Density Override
<!-- Auto-detect by viewport -->
<html>
<!-- Force mobile density -->
<html data-density="mobile">
Khi density thay đổi, toàn bộ tier tokens tự recalculate — bạn không cần viết media queries riêng!
8. Anti-Patterns — Những Lỗi Hay Gặp
❌ Hardcode spacing
/* ❌ SAI — hardcode pixel */
.my-card { padding: var(--spacing-4); gap: var(--spacing-3); }
/* ✅ ĐÚNG — dùng tier token */
.my-card { padding: var(--group-padding-default); gap: var(--group-stack-gap-default); }
❌ Dùng calc() cho gap/padding
/* ❌ SAI — bypass tier system */
.section { gap: calc(var(--spacing) * 5); }
/* ✅ ĐÚNG — dùng tier token */
.section { gap: var(--section-stack-gap-default); }
Exception:
calc()OK chowidth,height,position(non-gap/padding). Vàcalc(var(--spacing) * 0.5)OK cho micro-gap ≤ 2px.
❌ Sai tier
/* ❌ SAI — dùng page token cho card */
.my-card { padding: var(--page-padding-default); }
/* ✅ ĐÚNG — card = group tier */
.my-card { padding: var(--group-padding-default); }
❌ Đặt tên class theo data
/* ❌ SAI — tên theo business data */
.balance-card { ... }
.loan-section { ... }
/* ✅ ĐÚNG — tên theo elevation layer + UI role */
.card-account { ... }
.card-product { ... }
❌ Thiếu Elevation Map
/* ❌ SAI — code trước, nghĩ sau */
Viết HTML/CSS → debug spacing → hardcode fix
/* ✅ ĐÚNG — map trước, code sau */
Elevation Map → xác định layer → chọn tier → viết CSS
9. Quick Reference — Token Lookup Table
Spacing Tokens (đầy đủ)
| Tier | Stack Gap | Inline Gap | Grid Gap | Padding | Margin | Radius |
|---|---|---|---|---|---|---|
| Page | --page-stack-gap-{xlarge|large|default|small} |
--page-inline-gap-{default|small} |
--page-grid-{row|column}-gap |
--page-padding-{large|default|small} |
--page-margin-{large|default|small} |
--page-radius-{default|none} |
| Section | --section-stack-gap-{large|default|small} |
--section-inline-gap-{default|small} |
--section-grid-{row|column}-gap |
--section-padding-{large|default|small} |
--section-margin-{large|default} |
--section-radius-{default|small} |
| Group | --group-stack-gap-{large|default|small} |
--group-inline-gap-{default|small} |
--group-grid-{row|column}-gap |
--group-padding-{large|default|small} |
--group-margin-{large|default} |
--group-radius-{default|small} |
| Comp | --comp-stack-gap-{default|small} |
--comp-inline-gap-{default|small} |
— | --comp-padding-{large|default|small|xsmall} |
--comp-margin-default |
--comp-radius, --comp-radius-small, --comp-radius-capsule |
Color Tokens (Layer Contract)
| Layer | Background | Foreground | Border |
|---|---|---|---|
| Ground | --background, --canvas |
--foreground |
--border |
| Card | --card, --card-subtle |
--card-foreground |
--card-border, --card-border-subtle |
| Surface | --surface, --surface-subtle |
--surface-foreground |
--border |
10. Tóm Tắt — Công Thức Build Screen
Build Screen = Elevation Map + 4 Layers × 3 Patterns
Step 0: VẼ ELEVATION MAP
→ Xác định mỗi element = Ground / Card / Surface
Step 1: Page → Page shell, outer structure
Step 2: Section → Content regions, logical groups
Step 3: Group → Cards, panels, containers
Step 4: Comp → Buttons, inputs, interactive elements
Step 5: Overlay → Toast, modal, tooltip (Z-axis)
Mỗi layer: chọn LAYOUT PATTERN + áp TIER TOKENS
├── Stack? → flex-direction: column + {tier}-stack-gap
├── Inline? → flex-direction: row + {tier}-inline-gap
└── Grid? → display: grid + {tier}-grid-{row|column}-gap
Dev chỉ cần trả lời 3 câu hỏi cho mỗi element:
- Elevation layer nào? → ground- / card- / surface- (hoặc NO prefix cho comp)
- Tier nào? → page / section / group / comp
- Pattern nào? → stack / inline / grid
Token và spacing tự xử lý hết!