# Sheet

> A React sheet that slides in from any edge on a spring for filters, a cart, an inspector or phone navigation, swipeable to close on touch.

- Type: React component (Overlays)
- Access: Free (the source is open to everyone)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add sheet`
- Docs: https://wingo-ui.com/components/sheet

## Interaction

Click the trigger and the panel glides in from its edge on a spring while the page dims in step with it; it leaves the same way, a little faster. On a touch screen drag a side panel toward its edge, or a top or bottom sheet by its handle or header: it follows your finger, the dim lightens as it goes, and letting go past a quarter of its size (or a quick flick) closes it from right there, while a shorter drag springs back. Vertical scrolling inside a side panel stays native, and sideways scrollers and fields keep their own gestures. Escape, a click on the dimmed page or the X close it unless it is not dismissible; focus stays inside while it is open and goes back to the trigger after. A shortcut such as mod+j toggles it from anywhere. The header hairline appears once the body scrolls and the footer hairline while more is below. Non-modal sheets leave the page clickable and stay open while you work next to them. With reduced motion it fades instead of sliding.

## Installation

```bash
npx wingo-ui@latest add sheet
```

The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add https://wingo-ui.com/r/sheet.json`

Files:

- `components/ui/sheet.tsx`

npm dependencies: `npm i @radix-ui/react-dialog @radix-ui/react-visually-hidden lucide-react motion`

Wingo UI dependencies (installed automatically):

- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [useScrollEdges](https://wingo-ui.com/components/use-scroll-edges) ([Markdown](https://wingo-ui.com/components/use-scroll-edges.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))

## Usage

```tsx
import { SlidersHorizontal } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Sheet, SheetClose } from "@/components/ui/sheet"

export function ListingFilters({ results, onReset }: { results: number; onReset: () => void }) {
  return (
    <Sheet
      side="right"
      mobileSide="bottom"
      title="Filters"
      description="Biciclete · Cluj-Napoca"
      shortcut="mod+j"
      trigger={
        <Button variant="soft" leftIcon={<SlidersHorizontal />}>
          Filters
        </Button>
      }
      footer={
        <>
          <Button variant="ghost" onClick={onReset}>
            Reset
          </Button>
          <SheetClose asChild>
            <Button>Show {results} results</Button>
          </SheetClose>
        </>
      }
    >
      <p className="text-sm text-muted-foreground">Price, condition and delivery.</p>
    </Sheet>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultOpen?` | `boolean` | `false` | Starts open (uncontrolled). Pass open and onOpenChange instead to control it. |
| `open?` | `boolean` |  | Controlled open state. It lives in the root, so crossing 768px with mobileSide keeps the sheet open. |
| `onOpenChange?` | `(open: boolean) => void` |  | Called when the sheet opens or closes: the trigger, a swipe, Escape, an outside click, the X or the shortcut. |
| `title?` | `ReactNode` | `""` | The header title, also the sheet's accessible name. Without one, screen readers hear a hidden “Panel”; always pass a real one. The demo shows its sample while this is empty. |
| `description?` | `ReactNode` | `""` | A quieter line under the title, linked with aria-describedby. Without one the sheet opts out of the description (Radix's aria-describedby={undefined}). |
| `hideTitle?` | `boolean` | `false` | Keeps the title for screen readers only, for sheets that carry their own header (a menu with a logo). The body then starts at the top, so leave room for the X in its first row. |
| `closeLabel?` | `string` | `"Close"` | aria-label of the X button. Translate it together with the rest of your copy. |
| `trigger?` | `ReactNode` |  | The element that opens the sheet, rendered with asChild: a single element that takes a ref, like a Button. It gets aria-keyshortcuts when a shortcut is set. |
| `children?` | `ReactNode` |  | The body. It scrolls on its own (overscroll contained), pads the home indicator when it is the last part, and drives the header and footer hairlines. |
| `footer?` | `ReactNode` |  | Actions pinned under the body. Put the primary action last: it lands on the right from 640px and on top when the buttons stack full width on a phone. |
| `side?` | `"right" \| "left" \| "top" \| "bottom"` | `"right"` | The edge it slides in from. Left and right are logical, so an rtl page mirrors them (the swipe too). Top and bottom sheets get a grab handle on touch screens. |
| `mobileSide?` | `"auto" \| "right" \| "left" \| "top" \| "bottom"` | `"auto"` | The side below 768px. auto keeps side (a nav menu from the left stays on the left); bottom turns a right filter panel into a bottom sheet on phones. |
| `size?` | `"sm" \| "md" \| "lg" \| "xl" \| "full"` | `"md"` | Left and right: 320 / 400 / 540 / 720px wide, capped so a 48px sliver of the page stays tappable on a phone, or the whole screen. Top and bottom: as tall as the content up to 50 / 70 / 85 / 92% of the screen, or all of it. full always shows the X. |
| `variant?` | `"attached" \| "floating"` | `"attached"` | attached sits flush on its edge and rounds only the inner corners. floating hovers 8px off the screen edges and the safe areas with every corner rounded. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg"` | `"lg"` | Corner size: 0 / 12 / 16 / 24px. Attached top and bottom sheets take the bottom-sheet corner, 28px, at lg. Any other value falls back to lg. |
| `overlay?` | `boolean` | `true` | The dim backdrop. Turned off, a modal sheet still locks the page and closes on an outside click; it just does not darken it. |
| `showClose?` | `boolean` | `true` | The X in the top corner (a 44px target on touch). Always shown at size full, where no page is left to tap. |
| `showHandle?` | `boolean` | `true` | The grab handle of top and bottom sheets. It only appears on touch screens while the sheet can be swiped. |
| `modal?` | `boolean` | `true` | false: no overlay, no focus trap, no scroll lock. The page stays usable and clicks on it never close the sheet, like an inspector next to a list. |
| `dismissible?` | `boolean` | `true` | Outside clicks, Escape, swipes and the shortcut close it. false leaves only the X and your own actions (a SheetClose or setting open). |
| `swipeToClose?` | `boolean` | `true` | On touch screens, drag it toward its edge to close: side panels from anywhere on them, top and bottom sheets from the handle or header. Past a quarter of its size or a flick closes, less springs back. Mark a child data-sheet-no-drag to keep its own gestures. |
| `shortcut?` | `string` | `""` | A key combo in mod+k notation (⌘ on Apple, Ctrl elsewhere) that toggles the sheet from anywhere. The trigger gets aria-keyshortcuts. Empty binds nothing. |
| `container?` | `HTMLElement \| null` |  | Portal target. Defaults to document.body. |
| `classNames?` | `Partial<Record<"overlay" \| "content" \| "header" \| "title" \| "description" \| "body" \| "footer" \| "close" \| "handle", string>>` |  | Extra classes for the parts. The header, title, description, body and footer parts read theirs from the content, so this works with the parts API too. |
| `className?` | `string` |  | Extra classes for the panel (sheet-content). Set any corner with [--sheet-radius:20px]. |

## Parts

### SheetRoot (component)

The state and the Radix Dialog root, without DOM. Build custom sheets from the parts inside it. Props: { open, defaultOpen, onOpenChange, modal, dismissible, shortcut, container, children }.

### SheetTrigger (component)

Opens the sheet. Use asChild with your own button; it carries aria-keyshortcuts for the root's shortcut. Props: ComponentProps<typeof Dialog.Trigger>.

### SheetContent (component)

The portal, the overlay and the animated panel with its handle and X. Radix Content props (onOpenAutoFocus, onEscapeKeyDown, aria-describedby...) pass through. It focuses the panel itself on open; pass onOpenAutoFocus to focus a field. Props: { side, mobileSide, size, variant, radius, overlay, swipeToClose, showClose, showHandle, closeLabel, container, classNames } & Dialog.Content props.

### SheetHeader (component)

The title row (room left for the X); a hairline fades in under it once the body scrolls. On top and bottom sheets it is also a drag area. Props: { title?, description?, hideTitle?, children? }.

### SheetTitle (component)

The accessible name and description. visuallyHidden keeps the title for screen readers only. Props: ComponentProps<typeof Dialog.Title> & { visuallyHidden? }.

### SheetDescription (component)

The accessible name and description. visuallyHidden keeps the title for screen readers only. Props: ComponentProps<typeof Dialog.Description>.

### SheetBody (component)

The scroll region between header and footer. It reports its scroll edges for the hairlines. Takes div props.

### SheetFooter (component)

Pinned actions: stacked full width below 640px (last child on top), a right-aligned row above that wraps in a narrow panel. Takes div props.

### SheetClose (component)

Closes the sheet, also when it is not dismissible. Use asChild on a footer button. Props: ComponentProps<typeof Dialog.Close>.

### useSheet (hook)

The nearest sheet's state, e.g. to close it once a form has saved. Signature: () => { open: boolean; setOpen: (open: boolean) => void; side: SheetSide | undefined }.

## Examples

- **Navigation menu**: side="left" size="sm" with its own logo row and a hidden title. Every link closes it; swipe left on a phone.
- **Cart**: Quantities, removals that animate out, an empty state and a brand checkout pinned in the footer with the total.
- **Bottom sheet on phones**: mobileSide="bottom" keeps a right panel on desktop and becomes a bottom sheet with a handle below 768px. The side="bottom" one is capped at 560px on desktop with a className.
- **Sides and sizes**: Four edges, five sizes. Side panels always leave a tappable sliver on a phone, except full.
- **Floating**: variant="floating" hovers 8px off the edges with every corner rounded.
- **Non-modal inspector**: modal={false} and overlay={false}: edit a listing while the list stays clickable. From the left here, a bottom sheet on phones. Esc or × closes it.
- **Long content**: The header and footer stay pinned while the body scrolls; their hairlines follow the scroll position.

## See also

- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [Alert Dialog](https://wingo-ui.com/components/alert-dialog) ([Markdown](https://wingo-ui.com/components/alert-dialog.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [useScrollEdges](https://wingo-ui.com/components/use-scroll-edges) ([Markdown](https://wingo-ui.com/components/use-scroll-edges.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))

---

Canonical: https://wingo-ui.com/components/sheet
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
