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.jsonbottom-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-containerclip-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