Sheet
A modal panel that slides in from an edge of the screen. Use it for details, filters, and editors that need more room than a dialog but should keep the page behind in view. Focus stays inside until it closes. On a touch screen, swipe it back toward its edge to close it.
Usage
import { Button } from "@tulipfarm/sprout/button";
import {
Sheet,
SheetBody,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@tulipfarm/sprout/sheet";
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger render={<Button variant="outline" size="sm" />}>
<Filter aria-hidden="true" />
Filters (2)
</SheetTrigger>
<SheetContent side="bottom">
<SheetHeader>
<SheetTitle>Filter and sort</SheetTitle>
<SheetDescription>Results update as you choose.</SheetDescription>
</SheetHeader>
<SheetBody>…</SheetBody>
<SheetFooter>
<Button onClick={() => setOpen(false)}>Show results</Button>
<Button variant="ghost" size="sm" onClick={clear}>Clear filters</Button>
</SheetFooter>
</SheetContent>
</Sheet>Sheet works controlled with open and onOpenChange, or uncontrolled with a SheetTrigger. To keep it open while a save runs or while a second sheet is on top, ignore the change in onOpenChange and hide the corner button with showCloseButton={!saving}.
Sides
side is "right" (default), "left","top", or "bottom". Top and bottom sheets span the width, grow with their content up to 85% of the height, and lay out footer actions in a row from 640px.
Sides
Each side as Cloud opens it. Escape, the overlay, or a close button dismisses the sheet; nothing is saved.
Sizes
size sets the width of left and right sheets: "default" (24rem), "lg" (32rem), or "xl" (42rem). "full" covers the viewport from any side and respects safe areas. SheetTitle size="lg" is the larger title for product-style detail sheets.
Sizes
default 24rem, lg 32rem, xl 42rem, and full screen. Below 640px side sheets take 75% of the width, and lg and xl take all of it.
Swipe to close
Every sheet follows a touch swipe toward its side. A right sheet swipes right and a bottom sheet swipes down. Let go past half the panel, or flick it, and the sheet closes. A shorter swipe springs back. Scrolling a long body scrolls first and swipes only from the edge.
A mouse or pen drags the sheet only from the handle or empty panel space. Drags on text, fields, and buttons select and click as usual, so desktop forms are safe. showSwipeHandle adds a grab bar on the edge facing the page. It is off by default. Turn it on for bottom sheets on phones, where people expect it.
<Sheet>
<SheetTrigger render={<Button variant="outline" />}>Stock filters</SheetTrigger>
<SheetContent side="bottom" showSwipeHandle>
<SheetHeader>
<SheetTitle>Stock filters</SheetTitle>
<SheetDescription>Swipe down or press Show results to close.</SheetDescription>
</SheetHeader>
<SheetBody>…</SheetBody>
<SheetFooter>
<SheetClose render={<Button />}>Show results</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>Swipe to close
Swipe toward the edge on a touch screen. A short swipe springs back.
Cloud patterns
Put a custom bar and your own scroll region directly in SheetContent, or use SheetBody, which scrolls between the header and footer. initialFocus can target a title with tabIndex={-1}, finalFocus can return focus to a trigger ref, and keepMounted keeps drafts while the sheet is closed. A dialog or a second sheet can open on top.
Cloud patterns
Compositions from TulipFarm Cloud with sample data. Nothing leaves this page.
States
Show a skeleton with role="status" while data loads, an errorStatePanel with a retry, and a short sentence when nothing needs attention.
States
Loading, error, empty, and a list with a nested editor. The status sheet stays open while the editor on top is open.
API
Sheet, SheetTrigger, SheetPortal,SheetClose, SheetOverlay, SheetTitle, and SheetDescription wrap the Base UI drawer parts and take their props, including render. SheetHeader, SheetBody, and SheetFooter render a div and accept render, so a body can be the form. Sheet leaves out swipeDirection and snap points. The side on SheetContent sets the swipe direction.
SheetContent takes side, size, showCloseButton, closeLabel, keepMounted, container, showSwipeHandle, and every Base UI popup prop such as initialFocus and finalFocus. It sets data-slot="sheet-content" (overridable), data-side, and data-size. Its corner button has data-slot="sheet-close", so you can move it with a className selector. SheetTitle takes size ("default" or "lg").
Accessibility
The panel has role="dialog" and is named by SheetTitle. Every sheet needs a title, even if it is hidden with sr-only. Tab and Shift+Tab stay inside, and Escape or a click on the overlay closes it unless you ignore the change.
Opening slides 2.5rem and fades over 180ms. Closing takes 120ms. A swipe moves the panel with the finger, and the overlay fades as it goes. Motion turns off when reduced motion is requested. The swipe still works. The handle is hidden from assistive tech, so keep a close button or a footer action in every sheet.
Theme tokens
Sheets use --sprout-overlay, --sprout-surface, --sprout-line, --sprout-text, --sprout-muted, --sprout-shadow-overlay, and --sprout-radius-panel. They need no brand color, so they look the same with the neutral and TulipFarm themes in light and dark mode.