rapidUI

Sheet

Panel that slides in from a screen edge, built on Base UI Dialog.

Installation

pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/sheet.json

Installs to @/components/ui/sheet.

Manual installation

Install the dependencies:

pnpm add @base-ui/react@^1.8.0 class-variance-authority@^0.7.1 lucide-react@^1.48.0

Add the components it builds on: button.

Copy the source into your project:

components/ui/sheet.tsx
'use client'

import { Dialog as SheetPrimitive } from '@base-ui/react/dialog'
import { cva, type VariantProps } from 'class-variance-authority'
import { XIcon } from 'lucide-react'
import type * as React from 'react'

import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'

function Sheet({ ...props }: SheetPrimitive.Root.Props) {
	return <SheetPrimitive.Root data-slot="sheet" {...props} />
}

function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
	return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}

function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
	return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}

function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
	return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}

function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
	return (
		<SheetPrimitive.Backdrop
			data-slot="sheet-overlay"
			className={cn(
				'fixed inset-0 z-50 bg-black/50 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs',
				className,
			)}
			{...props}
		/>
	)
}

// Plain classes per side, not data-[side=…] variants: the attribute selector out-ranks a caller's className, so a
// drawer could not set its own width
const sheetContentVariants = cva(
	'fixed z-50 flex flex-col gap-4 overflow-y-auto bg-background bg-clip-padding text-sm text-foreground shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0',
	{
		variants: {
			side: {
				top: 'inset-x-0 top-0 h-auto max-h-dvh border-b data-ending-style:translate-y-[-2.5rem] data-starting-style:translate-y-[-2.5rem]',
				right:
					'inset-y-0 right-0 h-full w-3/4 border-l data-ending-style:translate-x-[2.5rem] data-starting-style:translate-x-[2.5rem] sm:max-w-sm',
				bottom:
					'inset-x-0 bottom-0 h-auto max-h-dvh border-t data-ending-style:translate-y-[2.5rem] data-starting-style:translate-y-[2.5rem]',
				left:
					'inset-y-0 left-0 h-full w-3/4 border-r data-ending-style:translate-x-[-2.5rem] data-starting-style:translate-x-[-2.5rem] sm:max-w-sm',
			},
		},
		defaultVariants: {
			side: 'right',
		},
	},
)

function SheetContent({
	className,
	children,
	side = 'right',
	showCloseButton = true,
	...props
}: SheetPrimitive.Popup.Props & {
	side?: NonNullable<VariantProps<typeof sheetContentVariants>['side']>
	showCloseButton?: boolean
}) {
	return (
		<SheetPortal>
			<SheetOverlay />
			<SheetPrimitive.Popup
				data-slot="sheet-content"
				data-side={side}
				className={cn(sheetContentVariants({ side }), className)}
				{...props}
			>
				{children}
				{showCloseButton && (
					<SheetPrimitive.Close
						data-slot="sheet-close"
						render={<Button variant="ghost" className="absolute top-3 right-3" size="icon-sm" />}
					>
						<XIcon />
						<span className="sr-only">Close</span>
					</SheetPrimitive.Close>
				)}
			</SheetPrimitive.Popup>
		</SheetPortal>
	)
}

function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
	return <div data-slot="sheet-header" className={cn('flex flex-col gap-0.5 p-4', className)} {...props} />
}

function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
	return <div data-slot="sheet-footer" className={cn('mt-auto flex flex-col gap-2 p-4', className)} {...props} />
}

function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
	return (
		<SheetPrimitive.Title
			data-slot="sheet-title"
			className={cn('font-heading text-base font-medium text-foreground', className)}
			{...props}
		/>
	)
}

function SheetDescription({ className, ...props }: SheetPrimitive.Description.Props) {
	return (
		<SheetPrimitive.Description
			data-slot="sheet-description"
			className={cn('text-sm text-muted-foreground', className)}
			{...props}
		/>
	)
}

export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription }

A sheet component that slides in from the edge of the screen. It can be triggered from any side of the viewport.

Features

  • Can be opened from any edge of the screen (top, right, bottom, left)
  • Supports controlled and uncontrolled states
  • Keyboard accessible
  • Focus management
  • Screen reader announcements
  • Slide and fade animations with a blurred backdrop
  • Built-in close button (toggle with showCloseButton)
  • Responsive design

Usage

import {
  Sheet,
  SheetTrigger,
  SheetContent,
  SheetHeader,
  SheetFooter,
  SheetTitle,
  SheetDescription,
  SheetClose,
} from "@/components/ui/sheet"
import { Button } from "@/components/ui/button"

export function ContactDetailsSheet() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>Edit contact details</SheetTrigger>
      <SheetContent side="right">
        <SheetHeader>
          <SheetTitle>Contact details</SheetTitle>
          <SheetDescription>We use these details to reach you about your cases.</SheetDescription>
        </SheetHeader>
        <div className="px-4">{/* Your content */}</div>
        <SheetFooter>
          <Button type="submit">Save changes</Button>
          <SheetClose render={<Button variant="outline" />}>Close</SheetClose>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  )
}

Examples

Basic Sheet

A resident's contact details, edited in a sheet from the right.

Position Variants

The sheet can slide in from different sides of the screen.

Top Sheet

A service notice about planned maintenance.

Bottom Sheet

Actions for a decision, like a mobile action sheet.

Left Sheet

Navigation for My pages, opened from an icon-only menu button.

Controlled Sheet

Selecting a case in the list opens its details. The sheet's open state follows the selected case.

Without Close Button

A filter sheet with the corner close button hidden with showCloseButton={false}. It closes with Escape or the footer buttons.

Custom Width

A translations sheet that needs more room. className="w-full sm:max-w-lg" replaces the default w-3/4 sm:max-w-sm: full width on phones, 512px from sm up.

Anatomy

<Sheet>
  <SheetTrigger />
  <SheetContent>
    <SheetHeader>
      <SheetTitle />
      <SheetDescription />
    </SheetHeader>
    <SheetFooter>
      <SheetClose />
    </SheetFooter>
  </SheetContent>
</Sheet>

Props

Sheet

The root component that manages the sheet's state.

SheetTrigger

The button that opens the sheet. By default, it's a button element.

<SheetTrigger>Open Sheet</SheetTrigger>

SheetContent

The component that contains the sheet's content. It can be positioned on any side of the screen.

<SheetContent side="right">
  {/* Your content */}
</SheetContent>

Props:

  • side: "top" | "right" | "bottom" | "left" (default "right")
  • showCloseButton: Whether to render the close button in the top-right corner (default true)
  • className: Additional CSS classes. Left and right sheets are w-3/4 with sm:max-w-sm by default; set your own width here to replace it, e.g. w-full sm:max-w-lg

Header and footer have their own p-4 padding; wrap body content in e.g. px-4.

SheetHeader

A container for the sheet's header content.

<SheetHeader>
  <SheetTitle>Sheet Title</SheetTitle>
  <SheetDescription>Sheet Description</SheetDescription>
</SheetHeader>

SheetFooter

A container for the sheet's footer content.

<SheetFooter>
  <Button>Save changes</Button>
</SheetFooter>

SheetClose

Closes the sheet. Use the render prop to render your own button.

<SheetClose render={<Button variant="outline" />}>Close</SheetClose>

SheetTitle

The title of the sheet. It is automatically announced by screen readers.

<SheetTitle>Contact details</SheetTitle>

SheetDescription

The description of the sheet. It is automatically announced by screen readers.

<SheetDescription>
  We use these details to reach you about your cases.
</SheetDescription>

Accessibility

The Sheet component follows WAI-ARIA guidelines for dialogs and modals:

  • Uses appropriate ARIA roles and attributes
  • Manages focus when opened and closed
  • Supports keyboard navigation (Escape to close)
  • Provides screen reader announcements
  • Implements a focus trap when opened

Keyboard Interactions

  • Space or Enter on trigger: Opens the sheet
  • Escape: Closes the sheet
  • Tab: Moves focus through interactive elements
  • Shift + Tab: Moves focus through interactive elements in reverse

Best Practices

  1. Always provide descriptive titles and descriptions
  2. Use appropriate side positioning based on content context
  3. Implement proper focus management
  4. Consider mobile responsiveness when choosing side positions
  5. Keep content concise and focused
  • Dialog - For centered modal dialogs
  • Drawer - For touch-friendly sliding panels
  • Sidebar - Uses Sheet for its mobile layout
  • Popover - For smaller floating content