Sheet React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
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.
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.
Sheet is a free React component in the React overlay components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add sheet, together with UIProvider, Button, Motion presets, useControllableState, useMediaQuery and 2 more and the npm packages @radix-ui/react-dialog, @radix-ui/react-visually-hidden, lucide-react and motion.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add sheetUsage
Import Sheet from @/components/ui/sheet.
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
defaultOpendefaultfalsebooleanStarts open (uncontrolled). Pass open and onOpenChange instead to control it.
openbooleanControlled open state. It lives in the root, so crossing 768px with mobileSide keeps the sheet open.
onOpenChange(open: boolean) => voidCalled when the sheet opens or closes: the trigger, a swipe, Escape, an outside click, the X or the shortcut.
titledefault""ReactNodeThe 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.
descriptiondefault""ReactNodeA quieter line under the title, linked with aria-describedby. Without one the sheet opts out of the description (Radix's aria-describedby={undefined}).
hideTitledefaultfalsebooleanKeeps 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.
closeLabeldefault"Close"stringaria-label of the X button. Translate it together with the rest of your copy.
triggerReactNodeThe 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.
childrenReactNodeThe 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.
footerReactNodeActions 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.
sidedefault"right""right" | "left" | "top" | "bottom"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.
mobileSidedefault"auto""auto" | "right" | "left" | "top" | "bottom"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.
sizedefault"md""sm" | "md" | "lg" | "xl" | "full"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.
variantdefault"attached""attached" | "floating"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.
radiusdefault"lg""none" | "sm" | "md" | "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.
overlaydefaulttruebooleanThe dim backdrop. Turned off, a modal sheet still locks the page and closes on an outside click; it just does not darken it.
showClosedefaulttruebooleanThe X in the top corner (a 44px target on touch). Always shown at size full, where no page is left to tap.
showHandledefaulttruebooleanThe grab handle of top and bottom sheets. It only appears on touch screens while the sheet can be swiped.
modaldefaulttruebooleanfalse: 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.
dismissibledefaulttruebooleanOutside clicks, Escape, swipes and the shortcut close it. false leaves only the X and your own actions (a SheetClose or setting open).
swipeToClosedefaulttruebooleanOn 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.
shortcutdefault""stringA key combo in mod+k notation (⌘ on Apple, Ctrl elsewhere) that toggles the sheet from anywhere. The trigger gets aria-keyshortcuts. Empty binds nothing.
containerHTMLElement | nullPortal target. Defaults to document.body.
classNamesPartial<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.
classNamestringExtra classes for the panel (sheet-content). Set any corner with [--sheet-radius:20px].
Parts
SheetRoot
componentThe 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
componentOpens the sheet. Use asChild with your own button; it carries aria-keyshortcuts for the root's shortcut. Props: ComponentProps<typeof Dialog.Trigger>.
SheetContent
componentThe 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
componentThe 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
componentThe accessible name and description. visuallyHidden keeps the title for screen readers only. Props: ComponentProps<typeof Dialog.Title> & { visuallyHidden? }.
SheetDescription
componentThe accessible name and description. visuallyHidden keeps the title for screen readers only. Props: ComponentProps<typeof Dialog.Description>.
SheetBody
componentThe scroll region between header and footer. It reports its scroll edges for the hairlines. Takes div props.
SheetClose
componentCloses the sheet, also when it is not dismissible. Use asChild on a footer button. Props: ComponentProps<typeof Dialog.Close>.
useSheet
hookThe 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.
Dependencies
Files
- components/ui/sheet.tsx
FAQ
Is the React Sheet component free?
Yes. The source is free and needs no account: copy it from the Code tab or install it with the CLI.
How do I install Sheet?
Run npx wingo-ui@latest add sheet. It also adds the Wingo UI items it builds on (UIProvider, Button, Motion presets, useControllableState, useMediaQuery and 2 more) and the npm packages @radix-ui/react-dialog, @radix-ui/react-visually-hidden, lucide-react and motion. With shadcn, run npx shadcn@latest add https://wingo-ui.com/r/sheet.json.
How does Sheet behave on phones?
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.
See also
Used in
More React overlay components
Articles using this component
- Nextjs Portfolio Template That Wins Clients
Build it: screens and apps9 min read
- Shadcn Navbar: Mega Menu, Mobile Menu, Sticky Header
Build it: screens and apps9 min read
- 100vh Mobile Fix: When to Use dvh, svh or lvh in Tailwind
Mobile UI in React11 min read
- React Bottom Sheet for the Web: Snap Points, Drag, Keyboard
Mobile UI in React9 min read


