Plan Tối Ưu Spacing System — Prisma Design System
Date: 2026-03-12 Status: 📋 PLAN — Chờ duyệt Sources:
- iOS 26 Liquid Glass Research
- Density Simplification
- Apple vs MD3 Comparison
- ChatGPT: So sánh gap/radius/sizing mobile (iOS, MD3, IBM Carbon)
- Notion: Spatial Rhythm trong UI (Airbnb/Apple/Stripe patterns)
0. Executive Summary
Sau khi tổng hợp 5 nguồn nghiên cứu, plan này đề xuất 3 phase tối ưu spacing system:
| Phase | Tên | Effort | Breaking? |
|---|---|---|---|
| Phase 1 | Spatial Rhythm Alignment | Small | ⚠️ Minor |
| Phase 2 | iOS 26 Future-Proofing | Medium | ❌ Non-breaking |
| Phase 3 | Density Architecture Simplification | Medium | ⚠️ Breaking (controlled) |
[!IMPORTANT] Core Insight từ research: UI premium không đến từ color/typography, mà từ spacing rhythm chặt. Prisma cần enforce 2× rule và 3-layer spacing model để đạt cảm giác "clean" như Airbnb/Apple/Stripe.
1. Hiện Trạng — Audit Nhanh
1.1 Spacing Scale hiện tại
Prisma primitives: --spacing = 4px
Scale dùng: ×0.5(2) → ×1(4) → ×2(8) → ×3(12) → ×4(16) → ×5(20) → ×6(24) → ×8(32)
1.2 So với Industry Standard
| Industry Pattern | Values | Prisma hỗ trợ? |
|---|---|---|
| 4-8-16 rule | 4, 8, 16 | ✅ Có |
| 2× rule (8→16→32) | 8, 16, 32 | ✅ Có (×2→×4→×8) |
| Big app convergence | 4, 8, 16, 24, 32, 48 | ⚠️ Thiếu 48 |
| 3-layer model | micro(4/8), comp(12/16), layout(24/32/48) | ⚠️ Mờ boundary |
1.3 Vấn đề phát hiện
| # | Vấn đề | Source | Severity |
|---|---|---|---|
| P1 | 2× rule bị phá ở nhiều tier — comp.stack-gap = 4px nhưng group.stack-gap = 12px (×3, không phải ×2) |
Notion Spatial Rhythm | 🟡 |
| P2 | Thiếu layer 48px — big app pattern cần spacing 48 cho page sections | ChatGPT + Notion | 🟡 |
| P3 | comp.padding-default mobile (12px) vs iOS 26 (16pt) — gap | iOS 26 Research | 🟡 |
| P4 | comp.radius mobile (8px) quá nhỏ cho iOS 26 capsule trend | iOS 26 Research | 🔴 |
| P5 | Thiếu capsule radius token (9999px) | iOS 26 Research | 🔴 |
| P6 | Thiếu concentric radius concept | iOS 26 Research | 🟡 |
| P7 | Thiếu comp.height tokens | Apple/MD3 Comparison | 🟡 |
| P8 | 3 density files → 2 files đã simplify nhưng architecture còn verbose | Density Simplification | 🟡 |
| P9 | Vertical rhythm chưa enforce — spacing không link với line-height | Notion Spatial Rhythm | 🟢 |
2. Phase 1: Spatial Rhythm Alignment ⭐ Quan trọng nhất
Goal: Enforce 2× rule + 3-layer spacing model → UI nhìn "premium" hơn. Effort: Small | Risk: Minor breaking
2.1 Định nghĩa 3 Spacing Layers
┌────────────────────────────────────────────────┐
│ Layer 3: LAYOUT │
│ Spacing: 24 / 32 / 48 │
│ Dùng cho: page margin, section gap, page gap │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Layer 2: COMPONENT │ │
│ │ Spacing: 12 / 16 │ │
│ │ Dùng cho: card padding, form gap, │ │
│ │ component internal padding │ │
│ │ │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ Layer 1: MICRO │ │ │
│ │ │ Spacing: 4 / 8 │ │ │
│ │ │ Dùng cho: icon-text gap, │ │ │
│ │ │ inline spacing, chip padding │ │ │
│ │ └──────────────────────────────────┘ │ │
│ └──────────────────────────────────────────┘ │
└────────────────────────────────────────────────┘
2.2 2× Rule Application
Pattern nhịp chính: 8 → 16 → 32
| Current Token | Hiện tại (Mobile) | Đề xuất | Lý do |
|---|---|---|---|
comp.stack-gap-default |
×1 = 4px | ×2 = 8px | 4→8: micro layer floor. Apple/MD3 đều ≥ 8px cho form gap |
comp.stack-gap-small |
×1 = 4px | ×1 = 4px | Giữ: micro layer min |
group.stack-gap-default |
×3 = 12px | ×4 = 16px | 8→16: đúng 2× rule |
group.stack-gap-small |
×1 = 4px | ×2 = 8px | Nâng lên micro base |
section.stack-gap-default |
×4 = 16px | ×4 = 16px | ✅ Giữ: đã đúng |
page.stack-gap-default |
×4 = 16px | ×6 = 24px | 16→24: layout layer, tạo nhịp rõ hơn |
page.stack-gap-large |
×6 = 24px | ×8 = 32px | 24→32: layout layer max |
Kết quả 2× rhythm:
BEFORE: 4 → 12 → 16 → 24 (nhảy không đều: ×3, ×1.33, ×1.5)
AFTER: 4 → 8 → 16 → 24 → 32 (nhảy đều: ×2, ×2, ×1.5, ×1.33)
micro comp layout
2.3 Thêm Spacing Token 48px
// Thêm vào page tier — cả mobile.tokens.json VÀ web.tokens.json
{
"page": {
"stack-gap-xlarge": {
"$type": "number",
"$value": "calc(var(--spacing) * 12)",
"$description": "48px — major section divider, hero spacing"
}
}
}
Lý do: Tất cả big app (Airbnb, Apple, Stripe, Uber) đều có spacing 48px cho section-level separation. Pattern chuẩn: 4, 8, 16, 24, 32, 48.
2.4 Comp Padding Adjustment (from Apple/MD3 research)
| Token | Mobile hiện tại | Mobile mới | Web hiện tại | Web mới |
|---|---|---|---|---|
comp.padding-default |
×3 = 12px | ×4 = 16px | ×3 = 12px | ×4 = 16px |
comp.padding-small |
×2 = 8px | ×2 = 8px ✅ | ×2 = 8px | ×3 = 12px |
comp.padding-xsmall |
×1 = 4px | ×1 = 4px ✅ | ×1 = 4px | ×2 = 8px |
[!WARNING]
comp.padding-default12→16px sẽ tăng size tất cả button/input. Đây là breaking change nhỏ nhưng needed — cả Apple (16pt) lẫn MD3 (12-16dp) đều chuộng giá trị này.
2.5 Summary Phase 1: Token Changes
## mobile.tokens.json changes
## comp tier
- "comp.stack-gap-default": "var(--spacing)" # 4px
+ "comp.stack-gap-default": "calc(var(--spacing) * 2)" # 8px
- "comp.padding-default": "calc(var(--spacing) * 3)" # 12px
+ "comp.padding-default": "calc(var(--spacing) * 4)" # 16px
## group tier
- "group.stack-gap-default": "calc(var(--spacing) * 3)" # 12px
+ "group.stack-gap-default": "calc(var(--spacing) * 4)" # 16px
- "group.stack-gap-small": "var(--spacing)" # 4px
+ "group.stack-gap-small": "calc(var(--spacing) * 2)" # 8px
## page tier
- "page.stack-gap-default": "calc(var(--spacing) * 4)" # 16px
+ "page.stack-gap-default": "calc(var(--spacing) * 6)" # 24px
- "page.stack-gap-large": "calc(var(--spacing) * 6)" # 24px
+ "page.stack-gap-large": "calc(var(--spacing) * 8)" # 32px
+ "page.stack-gap-xlarge": "calc(var(--spacing) * 12)" # 48px (NEW)
3. Phase 2: iOS 26 Future-Proofing
Goal: Thêm tokens cho iOS 26 Liquid Glass mà không break gì. Effort: Medium | Risk: Non-breaking (toàn bộ additive)
3.1 Capsule Radius Token
// Thêm vào CẢ mobile.tokens.json VÀ web.tokens.json
{
"comp": {
"radius-capsule": {
"$type": "number",
"$value": "9999px",
"$description": "Capsule/pill shape — iOS 26 primary button shape. Use for buttons, tags, nav items."
}
}
}
3.2 Nâng Comp Radius Mobile
| Token | Hiện tại | Đề xuất | Lý do |
|---|---|---|---|
comp.radius (mobile) |
--radius-xs (8px) |
--radius-sm (12px) |
iOS 26 fixed shape = 10-12pt. Apple HIG convention ~10pt. |
## mobile.tokens.json
- "comp.radius": "var(--radius-xs)" # 8px
+ "comp.radius": "var(--radius-sm)" # 12px
3.3 Concentric Radius CSS Helper
/* Thêm vào variables.css — utility cho nested containers */
:root {
--radius-concentric: max(0px, calc(var(--group-radius-default) - var(--group-padding-default)));
}
/* Usage example:
.card { border-radius: var(--group-radius-default); padding: var(--group-padding-default); }
.card > .inner { border-radius: var(--radius-concentric); }
*/
3.4 Component Height Tokens
// Thêm vào CẢ 2 density files
{
"comp": {
"height-default": {
"$type": "number",
"$value": "44px",
"$description": "Standard interactive height (Apple: 44pt)"
},
"height-large": {
"$type": "number",
"$value": "48px",
"$description": "Large button/input (MD3 comfortable target)"
},
"height-small": {
"$type": "number",
"$value": "36px",
"$description": "Compact interactive element"
}
}
}
3.5 Layout Tokens (iOS 26 specific)
// Thêm vào mobile.tokens.json
{
"layout": {
"tab-bar-height": {
"$type": "number",
"$value": "68px",
"$description": "iOS 26 floating tab bar height"
},
"nav-bar-height": {
"$type": "number",
"$value": "44px",
"$description": "iOS 26 floating nav bar height"
},
"safe-area-bottom": {
"$type": "number",
"$value": "34px",
"$description": "iPhone home indicator safe area"
}
}
}
4. Phase 3: Density Architecture Simplification
Goal: Simplify density switching (from density-simplification-research). Effort: Medium | Risk: Breaking (controlled migration)
4.1 Core Change
BEFORE:
tokens/density/compact.tokens.json ← DELETE
tokens/density/default.tokens.json ← DELETE
tokens/density/comfortable.tokens.json ← DELETE
tokens/density/mobile.tokens.json ← KEEP
tokens/density/web.tokens.json ← KEEP
CSS:
3 @layer blocks + responsive media query = ~290 lines
AFTER:
tokens/density/mobile.tokens.json ← app mode
tokens/density/web.tokens.json ← web mode (default)
CSS:
:root = web (default)
[data-mode="app"] = mobile override
= ~174 lines (-116 lines)
4.2 Data Attribute Migration
<!-- BEFORE -->
<body data-style-mode="compact">
<!-- AFTER -->
<body data-mode="app">
<!-- Web default — no attribute needed -->
<body data-brand="prisma" data-theme="light">
4.3 File Cleanup
| Action | Files |
|---|---|
| DELETE | compact.tokens.json, default.tokens.json, comfortable.tokens.json |
| KEEP | mobile.tokens.json (= app mode), web.tokens.json (= web/default) |
| UPDATE | variables.css — rewrite style-modes section |
| UPDATE | All docs referencing data-style-mode |
5. Final Spacing Scale — After All Phases
5.1 The "Prisma Rhythm" Scale
Token Scale (mobile mode):
MICRO LAYER
2px ×0.5 comp.padding-xsmall, min gap
4px ×1 comp.stack-gap-small, comp.inline-gap-small
8px ×2 comp.stack-gap-default, comp.padding-small, group.stack-gap-small
COMPONENT LAYER
12px ×3 group.stack-gap-default → 16px ✦, comp.padding-default → 16px ✦
16px ×4 section.padding-default, group.padding-default, comp.padding-default ✦
LAYOUT LAYER
20px ×5 section.padding-large
24px ×6 page.padding-large, page.stack-gap-default ✦
32px ×8 page.stack-gap-large ✦
48px ×12 page.stack-gap-xlarge (NEW ✦)
✦ = changed/new values
5.2 2× Rule Verification
Micro: 4 → 8 (×2) ✅
Comp: 8 → 16 (×2) ✅
Layout: 16 → 32 (×2) ✅
Full chain: 4 → 8 → 16 → 32 (perfect 2× rhythm)
Extras: 12, 24, 48 (×1.5 and ×1.5 fills for fine-tuning)
5.3 Radius Scale — After Phase 2
MOBILE:
comp.radius: 12px (was 8px) ← nâng cho iOS 26
comp.radius-capsule: 9999px ← NEW
group.radius-default: 12px ← giữ
section.radius-default: 16px ← giữ
page.radius-default: 16px ← giữ
WEB:
comp.radius: 12px ← giữ
comp.radius-capsule: 9999px ← NEW
group.radius-default: 16px ← giữ
section.radius-default: 24px ← giữ
page.radius-default: 24px ← giữ
6. Visual Before/After
6.1 Spacing Rhythm — Mobile Screen
BEFORE (inconsistent rhythm):
┌──────────────────────┐
│ margin: 16 │
│ ┌──────────────────┐ │
│ │ section gap: 16 │ │ ← section = page gap (flat)
│ │ ┌──────────────┐ │ │
│ │ │card gap: 12 │ │ │ ← ×0.75 (odd ratio)
│ │ │ icon→text: 4 │ │ │ ← ×0.33 (too tight)
│ │ │ form gap: 4 │ │ │ ← ×0.33 (too tight)
│ │ └──────────────┘ │ │
│ └──────────────────┘ │
└──────────────────────┘
AFTER (2× rhythm):
┌──────────────────────┐
│ margin: 16 │
│ ┌──────────────────┐ │
│ │ section gap: 24 │ │ ← ×1.5 (clear hierarchy)
│ │ ┌──────────────┐ │ │
│ │ │card gap: 16 │ │ │ ← ×2 from micro ✅
│ │ │ icon→text: 4 │ │ │ ← micro layer ✅
│ │ │ form gap: 8 │ │ │ ← ×2 from 4 ✅
│ │ └──────────────┘ │ │
│ └──────────────────┘ │
└──────────────────────┘
Rhythm: 4 → 8 → 16 → 24 (feels intentional, premium)
7. Implementation Priority
| # | Task | Phase | Effort | Impact | Priority |
|---|---|---|---|---|---|
| 1 | Enforce 2× rule — adjust stack-gap values | P1 | Small | 🔴 High — rhythm quality | P0 |
| 2 | Nâng comp.padding-default 12→16px | P1 | Small | 🔴 High — Apple/MD3 aligned | P0 |
| 3 | Thêm comp.radius-capsule (9999px) | P2 | Tiny | 🔴 High — iOS 26 ready | P0 |
| 4 | Nâng comp.radius mobile 8→12px | P2 | Small | 🟡 Medium — iOS 26 aligned | P1 |
| 5 | Thêm comp.height tokens (44/48/36) | P2 | Small | 🟡 Medium — standardize | P1 |
| 6 | Thêm page.stack-gap-xlarge (48px) | P1 | Tiny | 🟡 Medium — big section gap | P1 |
| 7 | Thêm concentric radius helper | P2 | Small | 🟡 Medium — iOS 26 nesting | P2 |
| 8 | Thêm layout. tokens* (tab/nav bar) | P2 | Small | 🟢 Low-Med — structured | P2 |
| 9 | Density arch simplification | P3 | Medium | 🟡 Medium — DX improvement | P2 |
| 10 | Tạo ios26.json style | Future | Large | 🟢 Future — full glass | P3 |
8. Quyết Định Cần Xác Nhận
[!IMPORTANT] Cần confirm trước khi implement:
- 2× rule enforcement: Đồng ý nâng
comp.stack-gap4→8px? Sẽ tăng khoảng cách giữa form elements. - comp.padding 12→16px: Chấp nhận buttons/inputs lớn hơn trên mobile?
- comp.radius 8→12px: Mobile components bo tròn hơn — OK?
- Capsule token: Thêm
comp.radius-capsule: 9999px— đồng ý? - 48px spacing: Thêm
page.stack-gap-xlargecho major sections? - Phase order: Bắt Phase 1 trước (rhythm) hay Phase 2 trước (iOS 26)?
- Density simplification (Phase 3): Xác nhận
data-mode="app"thaydata-style-mode="compact"?
9. Appendix: Research Cross-Reference
| Insight | Source | Applied in |
|---|---|---|
| 2× rule: group gap = 2× component gap | Notion Spatial Rhythm | Phase 1 §2.2 |
| 3-layer spacing: micro/component/layout | ChatGPT + Notion | Phase 1 §2.1 |
| Pattern 4,8,16,24,32,48 | ChatGPT (big app convergence) | Phase 1 §2.3 |
| Spacing rhythm > color/typography for premium feel | Notion insight | Phase 1 core thesis |
| Vertical rhythm = spacing as multiple of line-height | Notion | Phase 1 §2.1 (concept) |
| Optical spacing — sometimes "wrong math, right eye" | Notion | Acknowledged, not token-ized |
| iOS 26 capsule shape | iOS 26 Research | Phase 2 §3.1 |
| iOS 26 concentric radius | iOS 26 Research | Phase 2 §3.3 |
| iOS 26 spacious padding (+20%) | iOS 26 Research | Phase 1 §2.4 |
| comp.padding quá nhỏ vs Apple 16pt | Apple/MD3 Comparison | Phase 1 §2.4 |
| Touch target 44px already added | Apple/MD3 Comparison | ✅ Already in tokens |
| Density simplification web/app | Density Simplification | Phase 3 |
| Squint test spacing (Airbnb) | ChatGPT | Validation method |
| Density ladder (Grab/Gojek) | ChatGPT | Already doing via web/mobile split |