Prisma
Live Preview
Modal — Share Actions (medium detent ≈ 50%)
App content behind sheet...
Share
Choose how to share this item
Copy Link
Share
Save
Browser
Modal — Filter with Footer (large detent ≈ 92%)
Fullscreen — Search (100%, no radius)

--- description: Hướng dẫn Agent tự động tạo UI Component Bottom Sheet dựa trên Design Tokens, chuẩn Material 3 detent system & iOS HIG sheet pattern. Component ĐỘC LẬP — không phải variant của Drawer.

Bottom Sheet Component Generation Skill

Skill này hướng dẫn bạn (AI Agent) tạo component Bottom Sheet — panel nội dung trượt lên từ đáy màn hình, có hệ thống detent heights, drag handle, và slot-based content. Tách biệt hoàn toàn khỏi Drawer.

1. Mục tiêu (Objective)

Tạo component Bottom Sheet hỗ trợ 4 variants (modal, standard, persistent, fullscreen), detent system (peek/medium/large/full), drag handle resize, swipe-to-dismiss, slot-based content injection, và focus trap. Tích hợp 100% Design Tokens.

2. AI Context & Intent (Ngữ cảnh cho AI)

Khi nào dùng Bottom Sheet?

  • Action sheet: Share, export, print options (modal)
  • Filter & sort: Chips, sliders, date range (modal)
  • Detail preview: Quick view sản phẩm, place info (modal)
  • Quick form: 2-3 fields input nhanh (modal)
  • Search & select: Keyboard + list dài (fullscreen)
  • Map detail: Persistent info panel (standard)
  • Music player / tracker: Always visible (persistent)

⚠️ Phân biệt Bottom Sheet vs Drawer vs Modal vs Dialog (QUAN TRỌNG)

Tiêu chí Bottom Sheet Drawer Modal Dialog
Position Bottom edge, slide up Side edge (left/right) Center overlay Center overlay
Hướng Vertical only (↑↓) Horizontal (←→) or vertical None (centered) None (centered)
Detents ✅ (peek/medium/large/full) ❌ ❌ ❌
Drag handle ✅ Optional ❌ ❌ ❌
Swipe dismiss ✅ Swipe down ❌ ❌ ❌
Content Contextual / supplementary Navigation / long form Complex form / editor System feedback
Mobile Native pattern Hamburger menu Fullscreen or center Center
Tương đồng M3 BottomSheet, iOS Sheet M3 NavigationDrawer M3/iOS Dialog M3 AlertDialog

Decision Tree cho AI

text
Cần overlay ở đáy màn hình?
├─ Actions/share (3+ items) → Bottom Sheet (modal, medium)
├─ Filter/sort controls → Bottom Sheet (modal, medium→large)
├─ Detail preview + CTA → Bottom Sheet (modal, large)
├─ Quick form (2-3 fields) → Bottom Sheet (modal, large)
├─ Search + keyboard + list → Bottom Sheet (fullscreen)
├─ Map detail / persistent info → Bottom Sheet (standard)
├─ Always-visible tracker → Bottom Sheet (persistent)
├─ Side navigation (mobile) → Drawer (left)
├─ Detail/edit panel (desktop) → Drawer (right)
├─ Complex multi-field form → Modal
├─ Confirm/alert (1-2 actions) → Dialog
└─ Transient notification → Toast

3. Ngữ nghĩa & Phân loại (Semantics)

3.1. Variants

Variant Mô tả Scrim Detents Use case
modal Overlay + scrim, blocks background ✅ medium, large Action sheets, filters, quick forms
standard Coexists with background, no scrim ❌ peek, medium, large Map details, contextual info
persistent Always visible, snaps to detents ❌ peek, medium, large Music player, delivery tracker
fullscreen 100% screen, no radius, app bar header ✅ full only Search, long form, media view

3.2. Detent Heights (% screen)

Detent % Screen CSS Value When to use
peek 25% max-height: 25% Peek — summary, 1-2 items
medium 50% max-height: 50% Default modal open (M3 + iOS standard)
large 92% max-height: 92% Expanded — M3 top margin 72dp
full 100% max-height: 100%; border-radius: 0 Fullscreen variant

Rule: Không dùng fixed px cho height — luôn dùng % of container để responsive.

3.3. Sub-components (Slots)

text
┌─────────────────────────────────┐
│         Drag Handle             │ ← .surface-sheet__handle (optional), 32×4px
│─────────────────────────────────│
│ Header Title            [Close] │ ← .slot-header
│ Subtitle                        │
│─────────────────────────────────│ ← divider (--border)
│                                 │
│       Scrollable Content        │ ← .slot-body (SLOT injection point)
│       (list / form / custom)    │
│                                 │
│─────────────────────────────────│
│    [Secondary]    [Primary]     │ ← .slot-footer (optional)
│─────────────────────────────────│
│      safe area padding          │
└─────────────────────────────────┘
Multi-row Header (Search / Filter pattern)

Khi header cần chứa controls (search bar, filter chips) — dùng __header-row sub-component:

text
┌─────────────────────────────────┐
│         Drag Handle             │ ← .surface-sheet__handle
│─────────────────────────────────│
│ Header Title            [Close] │ ← .surface-bottomsheet__header-row
│ [🔍 Search input...          ] │ ← search/filter controls (pinned)
│─────────────────────────────────│ ← auto divider via border-bottom
│                                 │
│       Scrollable Results        │ ← .slot-body (only results scroll)
│                                 │
└─────────────────────────────────┘

Rule: Search/filter controls PHẢI nằm TRONG header (pinned), KHÔNG trong body (scrollable). Chuẩn iOS Sheet + Material 3.

Tái sử dụng __header-row

Pattern __header-row áp dụng cho bất kỳ sheet nào cần multi-row header. Chỉ cần:

  1. Wrap title + close trong .surface-bottomsheet__header-row
  2. Thêm controls bên dưới (search, filter chips, tabs, v.v.)

CSS tự kích hoạt column layout qua :has(.surface-bottomsheet__header-row).

Ví dụ use cases:

Use case Controls trong header Ví dụ
Search search-bar Search transactions, contacts...
Filter chip chips row All · Income · Expenses · Pending
Tabs tabs component Transfers · Payments · Requests
Search + Filter search-bar + chip row Search + category chips
html
<!-- Generic pattern — bất kỳ sheet nào -->
<div class="surface-bottomsheet__header">
    <div class="surface-bottomsheet__header-row">
        <span class="surface-bottomsheet__title">Title</span>
        <button class="surface-bottomsheet__close">✕</button>
    </div>
    <!-- Thêm bất kỳ controls nào ở đây — sẽ pinned, không scroll -->
    <div class="search-bar">...</div>
    <div class="flex gap-xs scroll-x">
        <button class="chip chip--primary">All</button>
        <button class="chip chip--secondary">Category</button>
    </div>
</div>
<div class="surface-bottomsheet__body">
    <!-- Chỉ phần này scroll -->
</div>
Part Class Mô tả
Handle .surface-sheet__handle Drag handle chip — kéo resize giữa detents
Header .slot-header Title + close button + optional subtitle
Header Row .surface-bottomsheet__header-row Row chứa title + close khi header là multi-row (column layout)
Body .slot-body Scrollable content area — slot injection point
Footer .slot-footer Action buttons — sticky bottom
Scrim .surface-sheet__scrim Background overlay (modal only)

Slot Architecture: .slot-body là injection point. JS switches content bằng slot name → inject HTML tương ứng. Mỗi slot define: title, subtitle, content, showFooter, footerButtons.

3.4. Slot Map (Figma ↔ Code)

📎 Source: slot-manifest.json → bottom-sheet · Layer: surface

Figma Slot data-slot CSS Class Required Accepts
Root bottom-sheet .surface-sheet ✅ —
Handle bottom-sheet-handle .surface-sheet__handle ❌ drag-handle
Header bottom-sheet-header .slot-header ❌ title-group
Header Row bottom-sheet-header-row .surface-bottomsheet__header-row ❌ title + close (multi-row)
Body bottom-sheet-body .slot-body ✅ list, form, search, *
Footer bottom-sheet-footer .slot-footer ❌ button-group
Scrim bottom-sheet-scrim .surface-sheet__scrim ❌ (modal only) overlay
html
<!-- HTML skeleton with data-slot -->
<div class="surface-sheet surface-sheet--modal" data-slot="bottom-sheet" role="dialog" aria-modal="true">
  <div class="surface-sheet__handle" data-slot="bottom-sheet-handle"></div>
  <div class="slot-header" data-slot="bottom-sheet-header">
    <span data-slot="bottom-sheet-title">Title</span>
    <button data-slot="bottom-sheet-close" aria-label="Close">✕</button>
  </div>
  <div class="slot-body" data-slot="bottom-sheet-body">
    <!-- scrollable content -->
  </div>
  <div class="slot-footer" data-slot="bottom-sheet-footer">
    <button class="btn btn--outline">Cancel</button>
    <button class="btn btn--primary">Confirm</button>
  </div>
</div>

4. Token Mapping

📦 Token values: Xem ATOMIC-MAPPING.md — mục bottom-sheet cho complete mapping.

Token Type Value Mô tả
bg color {base.surface} Container background
fg color {base.surface-foreground} Container text
radius number {radius.3xl} 28px top corners (M3 spec)
radius.fullscreen number 0 No radius when fullscreen
shadow shadow {elevation.shadow-md} Level-1 elevation
handle.bg color {base.muted-foreground} Drag handle color @ opacity 0.4
handle.width number 32px M3 drag handle spec
handle.height number 4px M3 drag handle spec
handle.radius number {radius.full} chip shape
handle.hit-area number 48px Min touch target (M3)
scrim color {base.background-overlay} Scrim for modal variant
header.title.font-size number {font.size.lg} 18px — overlay title (UX T11)
header.title.font-weight number {font.weight.semibold} Semibold title
header.subtitle.fg color {base.muted-foreground} Subtitle color
close.fg color {base.muted-foreground} Close button icon
close.hover color {state-layer.hover} Close button hover state
close.size number 32px Close button dimensions
divider color {base.border} Header/footer separator
footer.border color {base.border} Footer top separator
safe-area env env(safe-area-inset-bottom) iOS home indicator
max-width number 640px M3 responsive — tablet/desktop
transition.open string {duration.medium-2} {easing.standard-decelerate} Slide-up animation
transition.close string {duration.fast-2} {easing.standard-accelerate} Slide-down animation
z-index number {z-index.drawer} 1300 — stacking order
disabled.bg color {base.muted} Disabled state bg
disabled.fg color {base.muted-foreground} Disabled state fg
focus.ring color {focus.ring-color} Focus ring color

Height Standards (Cross-platform)

Metric M3 Apple HIG Prisma
Min height (standard) 56dp — 7%
Initial open (modal) 50% 50% (.medium) 50%
Expanded screen − 72dp (~92%) ~100% (.large) 92%
Fullscreen 100% 100% 100%

5. Props & API

typescript
interface BottomSheetProps {
  /** Variant — determines behavior & scrim */
  variant?: 'modal' | 'standard' | 'persistent' | 'fullscreen';
  /** Controlled open state */
  open: boolean;
  /** Close callback */
  onClose: () => void;
  /** Available detent heights */
  detents?: ('peek' | 'medium' | 'large' | 'full')[];
  /** Initial detent when opening */
  initialDetent?: 'peek' | 'medium' | 'large';
  /** Show drag handle */
  showHandle?: boolean; // default: true (false for fullscreen)
  /** Close on scrim tap (modal only) */
  closeOnOverlayClick?: boolean; // default: true
  /** Close on Escape key */
  closeOnEscape?: boolean; // default: true
  /** Swipe to dismiss */
  swipeToDismiss?: boolean; // default: true
  /** Lock background scroll (modal only) */
  lockBackgroundScroll?: boolean; // default: true
  /** Max width (tablet/desktop) */
  maxWidth?: string; // default: '640px'
  /** Children content */
  children: ReactNode;
}

interface BottomSheetHeaderProps {
  /** Title */
  title: string;
  /** Subtitle */
  subtitle?: string;
  /** Show close button */
  showCloseButton?: boolean; // default: true
}

interface BottomSheetFooterProps {
  children: ReactNode; // Buttons
}

6. Accessibility (a11y)

Requirement Implementation
Role role="dialog" (modal/fullscreen), role="complementary" (standard/persistent)
ARIA aria-modal="true" (modal), aria-label or aria-labelledby → title
Focus trap Modal/fullscreen: focus trapped inside. Standard/persistent: no trap
Initial focus First focusable element or close button
Return focus Khi close → trả focus về trigger element
Escape Close modal/fullscreen sheet
Drag handle role="slider", aria-label="Resize sheet", focusable in tab order
Screen reader Announce "Sheet opened" / "Sheet closed" via live region
Touch target Drag handle hit area: 48px. Close button: ≥44×44px
Reduced motion @media (prefers-reduced-motion: reduce) → instant show/hide, no slide
Tab order Focusable elements trong sheet giữ đúng tab order

7. Best Practices & Rules

  • Content: Bottom sheet CHỈ cho supplementary content — KHÔNG dùng cho primary content.
  • Actions rule: ≥ 3 actions → bottom sheet. 1-2 → dialog/menu.
  • No stacking: KHÔNG stack multiple sheets lên nhau.
  • Scroll vs dismiss: Content ở top → swipe dismisses. Content đã scroll → scroll first, rồi mới dismiss.
  • Drag handle: Luôn hiện cho modal/standard. Ẩn cho fullscreen.
  • Button size: Footer buttons PHẢI cùng size (BS-3). Mobile → btn-lg.btn--block. Xem button.md §4.7.
  • Portal: Render qua Portal vào <body> — tránh z-index/overflow issues.
  • Responsive: Desktop (≥840px) → tự chuyển thành Dialog hoặc Side Panel.
  • Không Hardcode: Mọi giá trị từ Token. KHÔNG hardcode px/colors.
  • Icon: Close button (x) CHỈ dùng Lucide icon từ assets/icons/. CẤM dùng text emoji. Xem icon.md mục 10.

7.1. Overlay Placement Rule (B11)

Sheet + scrim PHẢI là direct children của device-frame (hoặc body/portal), KHÔNG nằm bên trong container có clip-path hoặc overflow: hidden.

z-index hierarchy chuẩn:

Layer z-index Elements
OS Chrome 999 Dynamic Island, Home Indicator
Surface 60 (--local-z-surface) Bottom sheet, Modal, Dialog, Drawer
Scrim 50 (--local-z-scrim) Background overlay
Ground 10 (--local-z-ground) Status bar, App bar, Bottom nav

⚠️ Anti-pattern: Đặt sheet/scrim inside .app-screen-container có clip-path: inset(0) → bị clip, không phủ được status bar và bottom nav.

7.2. Safe Area Bottom (B12)

Bottom sheet phủ xuống đáy device — footer buttons bị home indicator che. PHẢI thêm safe area:

css
/* Footer có safe area */
.surface-bottomsheet__footer {
    padding-bottom: calc(var(--section-padding-default) + var(--local-iphone-safe-area-bottom));
}
/* Sheet không có footer → body cần safe area */
.surface-bottomsheet__body:last-child {
    padding-bottom: var(--local-iphone-safe-area-bottom);
}

7.3. Divider-Aware Body Padding (B13)

Áp dụng cho: Sheet, Drawer, Modal — mọi surface có header/body/footer slots.

Rule: Khi header hoặc footer có visible border (divider), body PHẢI có padding-y tương ứng để content không chạm divider. Khi KHÔNG có divider → padding-y = 0.

Divider Body padding Lý do
Header có border-bottom padding-top: var(--section-padding-default) Breathing room giữa divider và content
Footer có border-top padding-bottom: var(--section-padding-default) Breathing room trước footer
Không có divider padding-top: 0; padding-bottom: 0 Content flow tự nhiên vào spacing

CSS mechanism (auto-detect via adjacent sibling + :has()):

css
/* Sheet: header with divider → body gets padding-top */
.surface-bottomsheet__header:has(.surface-bottomsheet__header-row) + .surface-bottomsheet__body {
    padding-top: var(--section-padding-default);
}
/* Drawer: same pattern (future-ready) */
.surface-drawer__header[style*="border-bottom"] + .surface-drawer__body {
    padding-top: var(--section-padding-default);
}

⚠️ Anti-pattern: Content body chạm trực tiếp vào divider → thiếu breathing room → visual noise, khó đọc.

❌ Anti-patterns

Don't Do Why
Fixed px heights Percentage-based detents Responsive across devices
Stack sheets Close first, open second Cognitive overload
Primary content in sheet Use full page instead Sheet = supplementary only
Skip drag handle on modal Always show handle User expects swipe behavior
Nest drawer inside sheet Redesign UX Too many layers
Hardcode 28px radius Use var(--radius-3xl) Token system
Body content chạm divider Auto padding-y khi có divider (B13) Breathing room, visual clarity

8. Example Usage

jsx
{/* Modal — share actions */}
<BottomSheet
  open={isOpen}
  onClose={close}
  variant="modal"
  detents={['medium']}
  showHandle
>
  <BottomSheet.Header title="Share" subtitle="Choose how to share" />
  <BottomSheet.Body>
    <ShareGrid items={shareTargets} />
    <ActionList items={['Copy Link', 'Save', 'Print']} />
  </BottomSheet.Body>
</BottomSheet>

{/* Modal — filter with footer */}
<BottomSheet
  open={isOpen}
  onClose={close}
  variant="modal"
  detents={['medium', 'large']}
>
  <BottomSheet.Header title="Filter & Sort" subtitle="3 filters active" />
  <BottomSheet.Body>
    <FilterChips categories={['Food', 'Transport', 'Shopping']} />
    <PriceRange min={0} max={1000} />
  </BottomSheet.Body>
  <BottomSheet.Footer>
    <Button variant="outline" onClick={reset}>Reset</Button>
    <Button variant="primary" onClick={apply}>Apply Filters</Button>
  </BottomSheet.Footer>
</BottomSheet>

{/* Fullscreen — search */}
<BottomSheet
  open={isOpen}
  onClose={close}
  variant="fullscreen"
>
  <BottomSheet.Header title="Search Location" />
  <BottomSheet.Body>
    <SearchInput placeholder="Search locations..." />
    <LocationList items={locations} />
  </BottomSheet.Body>
</BottomSheet>

{/* Standard — map detail (non-modal) */}
<BottomSheet
  open={isOpen}
  onClose={close}
  variant="standard"
  detents={['peek', 'medium', 'large']}
  initialDetent="peek"
>
  <BottomSheet.Header title="Vincom Center" subtitle="72 Lê Thánh Tôn" />
  <BottomSheet.Body>
    <PlaceDetail place={selectedPlace} />
  </BottomSheet.Body>
  <BottomSheet.Footer>
    <Button variant="outline" onClick={share}>Share</Button>
    <Button variant="primary" onClick={navigate}>Directions</Button>
  </BottomSheet.Footer>
</BottomSheet>

9. CSS Naming Convention (Elevation-based)

text
.surface-sheet              ← Container (Surface layer)
.surface-sheet--modal       ← Modal variant (scrim + focus trap)
.surface-sheet--standard    ← Standard variant (no scrim)
.surface-sheet--persistent  ← Persistent variant (always visible)
.surface-sheet--fullscreen  ← Fullscreen variant (radius:0, 100%)
.surface-sheet--open        ← Open state (translateY: 0)
.surface-sheet--detent-peek     ← max-height: 25%
.surface-sheet--detent-medium   ← max-height: 50%
.surface-sheet--detent-large    ← max-height: 92%
.surface-sheet--detent-full     ← max-height: 100%
.surface-sheet__handle      ← Drag handle (32×4px chip)
.surface-sheet__header-row  ← Title + close row (inside multi-row header)
.surface-sheet__scrim       ← Background overlay (modal only)
.surface-sheet__close       ← Close button
.slot-header                ← Header area (generic slot)
.slot-body                  ← Scrollable content (generic slot)
.slot-footer                ← Action buttons (generic slot)

10. Cross-reference

Related File
Research knowledge/research/bottom-sheet-component-research.md
Atomic mapping ATOMIC-MAPPING.md → bottom-sheet section
Drawer (SEPARATE comp) drawer.md — side panels, NOT bottom sheet
Modal modal.md — center overlay
Dialog dialog.md — system feedback
Demo design-system/tokens/density/demo-bottom-sheet.html