Sheet
Installation
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
| Prop | Type | Default | Description |
|---|
open | boolean | — | Shows or hides the sheet, when you control it. |
defaultOpen | boolean | false | Shows the sheet on first render. |
onOpenChange | (open, details) => void | — | Called when the sheet opens or closes. |
modal | boolean | "trap-focus" | true | While 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. |
children | ReactNode | — | The trigger and the content. |
SheetTrigger
| Prop | Type | Default | Description |
|---|
render | ReactElement | — | Renders your own element in place of the plain button, such as a Button. |
disabled | boolean | false | Stops this trigger opening the sheet. |
children | ReactNode | — | The trigger's label. |
className | string | — | Extra classes for the trigger. |
SheetContent
| Prop | Type | Default | Description |
|---|
side | "top" | "bottom" | "start" | "end" | "end" | The edge the sheet slides in from. start and end follow the reading direction. |
showCloseButton | boolean | true | Shows the close button at the end of the header. |
closeLabel | string | "Close" | The close button's name for screen readers. Defaults to Close, or إغلاق on an Arabic page. |
container | HTMLElement | — | Where the sheet renders. Defaults to body. |
children | ReactNode | — | The header, the content and the footer. |
className | string | — | Extra classes for the sheet's panel. |
SheetHeader
| Prop | Type | Default | Description |
|---|
children | ReactNode | — | The title and the description. |
className | string | — | Extra classes for the header. |
SheetTitle
| Prop | Type | Default | Description |
|---|
children | ReactNode | — | What the sheet is for. |
className | string | — | Extra classes for the title. |
SheetDescription
| Prop | Type | Default | Description |
|---|
children | ReactNode | — | A line on what to do in the sheet. |
className | string | — | Extra classes for the description. |
SheetFooter
| Prop | Type | Default | Description |
|---|
children | ReactNode | — | The actions, the main action first. |
className | string | — | Extra classes for the footer. |
SheetClose
| Prop | Type | Default | Description |
|---|
render | ReactElement | — | Renders your own element in place of the plain button, such as a Button. |
onClick | (event) => void | — | Runs before the sheet closes. |
disabled | boolean | false | Disables the button. |
children | ReactNode | — | The button's label, such as Cancel. |
className | string | — | Extra classes for the button. |
Usage
Edit a profile
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
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
"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>
)
}