Skip to content
v2.5.0
English
Русский
Documentation

Sheet

Sheet is a compound component for panel-style overlays, usually bottom-mounted or mobile-first, with trigger, content, and handle parts.
sheet.tsx · 29 lines
import {
  Button,
  Heading,
  Icon,
  IconButton,
  Sheet,
  SheetClose,
  SheetContent,
  SheetHandle,
  SheetTrigger,
  Text,
} from '@vega-ui/react';
Browser support
Newly available 2024
  • Chrome 111
  • Edge 111
  • Firefox 128
  • Safari 16.4
  • Safari on iOS 16.4
Bundle size
Tested in
68 tests

Summary

Sheet is a compound component for panel-style overlays, usually bottom-mounted or mobile-first, with trigger, content, and handle parts. SheetClose turns your own button into a close button; headings and layout are plain markup you compose yourself. SheetContent renders a native <dialog> opened with showModal(), so the browser puts it in the top layer, makes the rest of the page inert, and draws the backdrop. It is the touch- and panel-oriented overlay in the family, especially when snap points or bottom-entry behavior matter.

Imports

import {
  Sheet,
  SheetTrigger,
  SheetContent,
  SheetClose,
  SheetHandle,
  type SheetProps,
} from '@vega-ui/react';

Exported Types

  • SheetProps
  • SheetTriggerProps
  • SheetHandleProps
  • SheetContentProps
  • SheetCloseProps

Minimal Composition

minimal-composition.tsx · 46 lines
import { useId, type CSSProperties } from 'react';
import {
  Button,
  Heading,
  Icon,
  IconButton,
  Sheet,
  SheetClose,
  SheetContent,
  SheetHandle,
  SheetTrigger,
  Text,

Required Parts

  • Sheet: root state and context provider
  • SheetTrigger: open action surface
  • SheetContent: modal sheet surface

Optional Parts

  • SheetHandle: drag or gesture affordance
  • SheetClose: closes the sheet when clicked; wraps your own button with asChild

Your Own Markup

  • a heading whose id is passed to SheetContent as aria-labelledby (or aria-label on SheetContent); the sheet must have a name
  • action buttons that answer the sheet inside a <form method="dialog">
  • a scrollable region: an element with overflow: auto

Composition Order

  1. Sheet
  2. SheetTrigger
  3. SheetContent
  4. SheetHandle
  5. Your heading, an optional SheetClose, and body

Variants

  • Bottom-sheet layouts versus general panel layouts
  • Trigger-driven versus controlled open state
  • Snap-point based sheets versus fully open sheets
  • closedBy on SheetContent: closed by any action, by Escape only, or not by the user at all
  • Dimmed versus transparent backdrop (overlay on SheetContent); the sheet stays modal either way
  • Mobile-first presentation versus broader cross-device panel usage

Migrating From 2.x

SheetHeader and SheetMain were removed: the library keeps only parts that carry behavior plain markup cannot express. SheetContent now has the padding they used to provide.

  • SheetHeader → a Heading with an id, referenced by aria-labelledby on SheetContent
  • SheetMain → plain content, or an element with overflow: auto for a scrollable region
  • New: SheetClose, a close part with no styles of its own (Sheet had no close button before)
  • useSheetContext() no longer returns contentId, and SheetTrigger no longer sets aria-controls

Edge Cases

  • Validate gesture affordances on touch devices.
  • Snap points should be tested against real content height.
  • Nested overlays need focus restoration and stacking checks.
  • Non-closable sheets need especially clear exit paths.

Common Mistakes

  • Leaving SheetContent without aria-labelledby or aria-label.
  • Treating a sheet like a desktop modal dialog.
  • Using snap points without checking content usability at each stop.
  • Forgetting to test gesture, focus, and dismissal together.

Examples

Basic

Basic: share sheet with heading and close button

The sheet fits its content and closes with the round button, a swipe down, a backdrop click or Escape. The heading names it through aria-labelledby, the address describes it through aria-describedby, and the copy result is announced through a role="status" line.

sheet-with-heading-and-close-button.tsx · 71 lines
import { useId, useState, type CSSProperties } from 'react';
import {
  Button,
  Heading,
  Icon,
  IconButton,
  Sheet,
  SheetClose,
  SheetContent,
  SheetHandle,
  SheetTrigger,
  Text,

Controlled/Stateful

Controlled/Stateful: snap point in your state

With activeSnapPoint and onChangeActiveSnapPoint the snap point lives in your state, so the Peek, Half and Full buttons move the sheet and a drag updates the pressed button. height: 100% makes the sheet as tall as the viewport, so all three snap points apply.

controlled-snap-point.tsx · 54 lines
import { useId, useState } from 'react';
import {
  Button,
  Heading,
  Sheet,
  SheetContent,
  SheetHandle,
  SheetTrigger,
  Text,
} from '@vega-ui/react';

const snapPoints: Array<`${number}px` | number> = ['180px', 0.5, 1];

Controlled/Stateful: picker that closes on choice

The sheet's open is controlled, so choosing a delivery slot stores it and closes the sheet in one handler. The trigger shows the current choice, and aria-pressed marks it inside the sheet.

mobile-like-picker-surface.tsx · 71 lines
import { useId, useState, type CSSProperties } from 'react';
import {
  Button,
  Heading,
  Icon,
  IconButton,
  Sheet,
  SheetClose,
  SheetContent,
  SheetHandle,
  SheetTrigger,
  Text,

Form/Integration

Form/Integration: feedback form

A <form method="dialog"> with a number field limited by min, max and step and an optional note. The browser validates the tip, a valid submit is read with FormData and closes the sheet; on phones the sheet rises above the on-screen keyboard.

sheet-with-form-content.tsx · 72 lines
import { useId, useState, type CSSProperties, type FormEvent } from 'react';
import {
  Button,
  Heading,
  Icon,
  IconButton,
  Label,
  Sheet,
  SheetClose,
  SheetContent,
  SheetHandle,
  SheetTrigger,

Layout/Overlay

Layout/Overlay: long list with snap points

Forty places make the sheet as tall as the viewport, so it rests at 160 px, half and full height. The list is the scrollable element with overscroll-behavior: contain; it scrolls once the sheet reaches the top, and dragging down at its top moves the sheet.

long-scrollable-sheet.tsx · 47 lines
import { useId } from 'react';
import {
  Button,
  Heading,
  Sheet,
  SheetContent,
  SheetHandle,
  SheetTrigger,
  Text,
} from '@vega-ui/react';

const names = ['Blue Harbor Cafe', 'Little Fern', 'Kiln & Co', 'Northside Roasters', 'The Daily Grind', 'Saltwater Bakery'];

Layout/Overlay: sheet over a sheet

The place sheet opens a second sheet for choosing a list. Each list is a submit button in a <form method="dialog">, so a choice closes only the inner sheet and the place shows where it was saved.

nested-sheets.tsx · 62 lines
import { useId, useState, type CSSProperties } from 'react';
import {
  Button,
  Heading,
  Icon,
  IconButton,
  Sheet,
  SheetClose,
  SheetContent,
  SheetHandle,
  SheetTrigger,
  Text,

Error

Error: invalid promo code

The code is checked on submit, so the form prevents the default and the sheet is controlled: an unknown code keeps the sheet open with an error linked through aria-describedby and aria-invalid, a known one (WELCOME10, FRIDAY20) applies the discount and closes it.

form-error.tsx · 107 lines
import { useId, useState, type CSSProperties, type FormEvent } from 'react';
import {
  Button,
  Heading,
  HelperText,
  Icon,
  IconButton,
  Label,
  Sheet,
  SheetClose,
  SheetContent,
  SheetHandle,

Disabled

Disabled: trigger disabled while the cart is empty

The Review order trigger is disabled until the cart has an item, and the hint below it is linked with aria-describedby while it is disabled. Add a flat white to enable it.

disabled-trigger.tsx · 69 lines
import { useId, useState } from 'react';
import {
  Button,
  Heading,
  HelperText,
  Icon,
  IconButton,
  Sheet,
  SheetContent,
  SheetHandle,
  SheetTrigger,
  Text,

Edge

Edge: stepped snap points

With steppedSnapPoints a drag or flick moves the sheet one snap point at a time, however fast it is, through quarter steps of the screen. The line under the heading follows onChangeActiveSnapPoint.

stepped-snap-points.tsx · 43 lines
import { useId, useState } from 'react';
import {
  Button,
  Heading,
  Sheet,
  SheetContent,
  SheetHandle,
  SheetTrigger,
  Text,
} from '@vega-ui/react';

const snapPoints: Array<`${number}px` | number> = [0.25, 0.5, 0.75, 1];

Edge: step that must be answered

With closedBy='none' a swipe, a backdrop click or Escape does not close the sheet, and there is no close button; only the two answers in the <form method="dialog"> do. The page reads the chosen returnValue in onClose.

blocking-step-sheet.tsx · 41 lines
import { useId, useState } from 'react';
import {
  Button,
  Heading,
  Sheet,
  SheetContent,
  SheetTrigger,
  Text,
} from '@vega-ui/react';

export default function BlockingStepSheet() {
  const titleId = useId();

Anatomy

Overview

Sheet owns open state, dismissal behavior, and panel coordination across its exported parts. It is a compound component because the root and child parts share one interaction model.

Required Parts

Sheet

Required. Owns open state and interaction lifecycle.

SheetTrigger

Required for trigger-driven flows. Opens the sheet from a button or other action surface.

SheetContent

Required. Renders the visible panel surface.

  • Owns: the native modal <dialog>, its ::backdrop, drag gestures, and open and close transitions
  • Renders into document.body, so no portal part is needed

Optional Parts

SheetHandle

Optional drag affordance, especially useful for mobile-first sheets. With several snap points it is a button that moves the sheet to the next snap point; otherwise it is hidden from assistive technology.

SheetClose

Closes the sheet when clicked.

  • Required: no
  • Renders a <button type="button"> with no styles, icon, or label; with asChild it renders your own button instead
  • Calls onChangeOpen(false) before your onClick, so the close transition plays and a controlled parent can keep the sheet open
  • Give an icon-only close button an aria-label

Your Own Markup

The library does not ship header or body parts. They carry no behavior that plain markup cannot express, so you compose them yourself. SheetContent provides the horizontal and bottom padding.

Heading

Names the sheet. A name is required: reference a heading's id with aria-labelledby on SheetContent, or set aria-label on SheetContent.

Scrollable region

An element with overflow: auto. As a flex item of SheetContent it shrinks to the available height on its own.

Action buttons

Buttons that answer the sheet go in a <form method="dialog">: submitting it closes the sheet through onChangeOpen(false), and the submit button's value becomes the dialog's returnValue. Use SheetClose for a standalone close button.

Composition Order

  1. Sheet
  2. SheetTrigger
  3. SheetContent
  4. SheetHandle
  5. Your heading, an optional SheetClose, and body

Valid Composition Patterns

minimal-composition.tsx · 46 lines
import { useId, type CSSProperties } from 'react';
import {
  Button,
  Heading,
  Icon,
  IconButton,
  Sheet,
  SheetClose,
  SheetContent,
  SheetHandle,
  SheetTrigger,
  Text,

Invalid Composition Patterns

SheetContent outside Sheet

This breaks state sharing and dismissal behavior.

Content without a name

SheetContent does not name itself. Without aria-labelledby or aria-label, screen readers announce an unnamed dialog.

Complex desktop modal flow in a cramped bottom sheet

That usually means Dialog or Drawer is the better pattern.

Snap-point layout without testing real content height

Intermediate states can become unusable if form controls or actions are clipped.