Sheet

Installation

bash
npx @smicolon/fasla-ui@latest add sheet

Run init once per project before adding components. See Installation.

Sheet is built on Base UI, so it needs the @base-ui/react package, and its close button uses button. add installs both with the component, and so does the shadcn CLI.

Preview

Press a button to open the sheet from that side. Tab stays inside it, and Escape, the close button or a press on the overlay closes it.

Side

side sets the edge the sheet slides in from. end, the default, suits settings, details and filters. start suits navigation menus. Both are 384px wide and as tall as the window. start and end follow the reading direction: end is the right edge on an English page and the left edge on an Arabic one, and the sheet slides from that edge. top and bottom are as wide as the window and as tall as their content. Keep bottom for short actions and quick choices on a phone, and top for a notice or a toolbar.

Content and footer

Anything you place between SheetHeader and SheetFooter is the sheet's content, 16px from each. Give it 16px on each side with a class such as px-4, as the fields below have. When the content is longer than the sheet, only the content scrolls: the header stays at the top and the footer at the bottom. SheetFooter stacks its buttons full width, 16px apart, with the main action first.

Close button

The close button sits at the end of the header, 16px from the edge, on the middle of the title's first line. Its name for screen readers follows the page's language, Close or إغلاق; set closeLabel to change it. The header leaves 48px at its end, so a long title wraps before it reaches the button. To close from your own button, such as Cancel, wrap it in SheetClose. Set showCloseButton to false only when another button in the sheet closes it.

Long title

A long title wraps inside the header and the close button stays on its first line. Everything under it moves down to make room. Keep titles to a few words where you can, so the content starts high in the sheet.

Accessibility

  • The sheet has the dialog role. Its title names it and its description describes it, so a screen reader reads both when it opens.
  • When the sheet opens, focus moves to its first control, such as the first field. Tab and Shift+Tab stay inside the sheet, and focus returns to the trigger when it closes.
  • Escape, the close button, a SheetClose button and a press on the overlay all close the sheet.
  • The close button is named Close, or إغلاق on an Arabic page, and its × is hidden from screen readers.
  • While the sheet is open, the page behind it can't be reached with the keyboard or the pointer, and it doesn't scroll.
  • The sheet slides in from its edge. When reduced motion is on, it fades in and out instead, without moving.
  • The sheet takes the page's direction, language and font, though it renders at the end of body. On an Arabic page end opens from the left and start from the right, and the close button moves with the end edge. Direction needs no prop.

Props

Sheet

PropTypeDefaultDescription
openboolean—Shows or hides the sheet, when you control it.
defaultOpenbooleanfalseShows the sheet on first render.
onOpenChange(open, details) => void—Called when the sheet opens or closes.
modalboolean | "trap-focus"trueWhile the sheet is open, keeps focus inside it and stops the page scrolling. Pass false to let focus leave the sheet and the page scroll, or "trap-focus" to keep focus inside without locking the scroll.
childrenReactNode—The trigger and the content.

SheetTrigger

PropTypeDefaultDescription
renderReactElement—Renders your own element in place of the plain button, such as a Button.
disabledbooleanfalseStops this trigger opening the sheet.
childrenReactNode—The trigger's label.
classNamestring—Extra classes for the trigger.

SheetContent

PropTypeDefaultDescription
side"top" | "bottom" | "start" | "end""end"The edge the sheet slides in from. start and end follow the reading direction.
showCloseButtonbooleantrueShows the close button at the end of the header.
closeLabelstring"Close"The close button's name for screen readers. Defaults to Close, or إغلاق on an Arabic page.
containerHTMLElement—Where the sheet renders. Defaults to body.
childrenReactNode—The header, the content and the footer.
classNamestring—Extra classes for the sheet's panel.

SheetHeader

PropTypeDefaultDescription
childrenReactNode—The title and the description.
classNamestring—Extra classes for the header.

SheetTitle

PropTypeDefaultDescription
childrenReactNode—What the sheet is for.
classNamestring—Extra classes for the title.

SheetDescription

PropTypeDefaultDescription
childrenReactNode—A line on what to do in the sheet.
classNamestring—Extra classes for the description.

SheetFooter

PropTypeDefaultDescription
childrenReactNode—The actions, the main action first.
classNamestring—Extra classes for the footer.

SheetClose

PropTypeDefaultDescription
renderReactElement—Renders your own element in place of the plain button, such as a Button.
onClick(event) => void—Runs before the sheet closes.
disabledbooleanfalseDisables the button.
childrenReactNode—The button's label, such as Cancel.
classNamestring—Extra classes for the button.

Usage

Edit a profile

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

export function EditProfile() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>Edit profile</SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Edit profile</SheetTitle>
          <SheetDescription>Make changes to your profile here. Click save when you're done.</SheetDescription>
        </SheetHeader>
        <div className="grid gap-4 px-4">
          <Input label="Name" defaultValue="Nadia Rahman" />
          <Input label="Username" defaultValue="nadia.rahman" />
        </div>
        <SheetFooter>
          <Button type="submit">Save changes</Button>
          <SheetClose render={<Button variant="outline" tone="primary" />}>Cancel</SheetClose>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  )
}

Open a menu from the start

tsx
import { MenuIcon } from "lucide-react"
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"
import { Button } from "@/components/ui/button"

export function NavigationMenu() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="ghost" size="icon-md" aria-label="Menu" />}>
        <MenuIcon />
      </SheetTrigger>
      <SheetContent side="start">
        <SheetHeader>
          <SheetTitle>Fasla</SheetTitle>
          <SheetDescription>Go to any part of your workspace.</SheetDescription>
        </SheetHeader>
        <nav className="grid gap-1 px-4">
          <a href="/">Home</a>
          <a href="/projects">Projects</a>
          <a href="/settings">Settings</a>
        </nav>
      </SheetContent>
    </Sheet>
  )
}

Close after saving

tsx
"use client"

import * as React from "react"
import {
  Sheet,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"

export function EditProfileAndSave({ onSave }: { onSave: (name: string) => Promise<void> }) {
  const [open, setOpen] = React.useState(false)
  const [name, setName] = React.useState("Nadia Rahman")

  return (
    <Sheet open={open} onOpenChange={setOpen}>
      <SheetTrigger render={<Button variant="outline" />}>Edit profile</SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Edit profile</SheetTitle>
          <SheetDescription>Make changes to your profile here. Click save when you're done.</SheetDescription>
        </SheetHeader>
        <div className="px-4">
          <Input label="Name" value={name} onChange={(event) => setName(event.target.value)} />
        </div>
        <SheetFooter>
          <Button
            onClick={async () => {
              await onSave(name)
              setOpen(false)
            }}
          >
            Save changes
          </Button>
          <SheetClose render={<Button variant="outline" tone="primary" />}>Cancel</SheetClose>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  )
}