Prisma

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à:

text
🏢 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:

  1. Vẽ Elevation Map → xác định mỗi element thuộc layer nào (Ground / Card / Surface)
  2. Chọn shell → toàn bộ page structure
  3. Chia vùng → các khu vực nội dung (sections)
  4. Đặt container → cards, panels (modules)
  5. Fill chi tiết → buttons, inputs (components)
  6. 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:

text
┌─────────────────────────────────────────────┐
│ 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:

text
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

text
.[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

text
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

  1. Token constraint: KHÔNG dùng hardcoded RGB/HEX cho background, color, border-color. PHẢI dùng token theo layer contract
  2. Item-level (Comp tier): Buttons, badges, avatars KHÔNG dùng layer prefix — chúng inherit từ parent layer
  3. 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.

html
<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.

html
<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:

css
.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.

html
<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:

css
.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.

html
<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:

css
.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:

text
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):

text
┌─────────────────────────────────────┐
│ 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

html
<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:

css
/* 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

html
<!-- 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

css
/* ❌ 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

css
/* ❌ 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 cho width, height, position (non-gap/padding). Và calc(var(--spacing) * 0.5) OK cho micro-gap ≤ 2px.

❌ Sai tier

css
/* ❌ 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

css
/* ❌ 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

text
/* ❌ 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

text
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:

  1. Elevation layer nào? → ground- / card- / surface- (hoặc NO prefix cho comp)
  2. Tier nào? → page / section / group / comp
  3. Pattern nào? → stack / inline / grid

Token và spacing tự xử lý hết!