Sheet
A sheet slides a panel in from an edge of the window, such as a navigation drawer or a details panel.A sheet opens a panel against one edge of the window and dims the page behind it. Use it for navigation on a small screen, or for details and edits that belong beside the content people came from. A short task in the middle of the window is a Dialog, and a column that stays open beside the page is part of the Shell.
Playground
Usage
import { Button, Flex, Sheet, Text } from '@kushagradhawan/kookie-ui';
export function OrderDetails({ order }: { order: { id: string; status: string; total: string } }) {
return (
<Sheet.Root>
<Sheet.Trigger>
<Button>View order</Button>
</Sheet.Trigger>
<Sheet.Content side="end">
<Flex direction="column" gap="5" height="100%">
<Flex direction="column" gap="1">
<Sheet.Title>Order {order.id}</Sheet.Title>
<Sheet.Description>{order.status}</Sheet.Description>
</Flex>
<Text size="2">Total: {order.total}</Text>
<Flex gap="2" justify="end" mt="auto">
<Sheet.Close>
<Button emphasis="quiet" color="gray">Close</Button>
</Sheet.Close>
<Button emphasis="loud">Refund order</Button>
</Flex>
</Flex>
</Sheet.Content>
</Sheet.Root>
);
}Anatomy
<Sheet.Root>
<Sheet.Trigger />
<Sheet.Content>
<Sheet.Title />
<Sheet.Description />
<Sheet.Close />
</Sheet.Content>
</Sheet.Root>- Root: Holds the open state. A sheet is always modal.
- Trigger: The element that opens the sheet. It takes one child element, usually a Button.
- Content: The panel, rendered in a portal against one edge, with a dimmed overlay behind it.
- Title: The sheet's name. It renders a Heading at size 4, medium weight.
- Description: A line under the title. It renders Text at size 2 with
emphasis="medium". - Close: Closes the sheet when its child is pressed. It takes one child element.
Best practices
Open it from the edge people expect. Navigation comes from the start edge, where the menu button sits. Details and inspectors come from the end edge. A list of actions on a phone comes from the bottom.
Keep the page it came from in mind. A sheet is for content that relates to the page behind it. A flow that leaves that page behind belongs on its own page.
Give it a title, even a hidden one. The title names the panel for screen readers. Use Sheet.Title inside VisuallyHidden when the design has no visible heading.
Put the main action at the end of the panel. One loud button, with a quiet Close beside it. Two loud buttons in a narrow panel give people no answer to "which one do I press?"
Don't use a sheet for a quick question. A confirmation that slides in from the side moves people's eyes away from what they pressed. Use an Alert Dialog.
Side
side sets the edge the panel opens against. start is the default.
| Side | Opens against | Use it for |
|---|---|---|
start | The leading edge, left in left-to-right text | Navigation |
end | The trailing edge, right in left-to-right text | Details, inspectors and edit forms |
top | The top edge | Content tied to a top bar, such as search |
bottom | The bottom edge | Actions and pickers on a phone |
left and right are accepted too. They become start and end, so the panel follows the reading direction.
Size
size sets the padding and the widest a start or end sheet can be, on the same steps as Dialog: 360, 440, 560 and 720 pixels. Size 2 is the default, and an unset size follows the Theme size.
A start or end sheet fills the window's height and takes 90% of its width up to that cap, so it fills a phone and stops on a desktop. The corners on the sheet's own edge are square, and the two facing the page take the size's corner. Top and bottom sheets span the full width and take 75% of the height. Set width or height to change these. A width above the cap needs maxWidth too.
Examples
Navigation drawer
Open the app's navigation from a menu button on small screens.
<Sheet.Root>
<Sheet.Trigger>
<Button iconOnly emphasis="quiet" aria-label="Menu">
<HugeiconsIcon icon={Menu01Icon} strokeWidth={1.75} />
</Button>
</Sheet.Trigger>
<Sheet.Content side="start" width="280px" aria-label="Navigation">
<Flex direction="column" gap="1">
<Button emphasis="quiet" fullWidth asChild>
<Link href="/projects">Projects</Link>
</Button>
<Button emphasis="quiet" fullWidth asChild>
<Link href="/settings">Settings</Link>
</Button>
</Flex>
</Sheet.Content>
</Sheet.Root>Responsive width
width accepts a responsive object, so a details panel can fill a phone and stay narrow on a desktop.
<Sheet.Content side="end" width={{ initial: '100%', md: '400px' }}>Bottom action sheet
Set height="auto" so a bottom sheet is only as tall as its actions.
<Sheet.Content side="bottom" height="auto" aria-label="Share">
<Flex direction="column" gap="2">
<Button fullWidth>Copy link</Button>
<Sheet.Close>
<Button emphasis="quiet" color="gray" fullWidth>Cancel</Button>
</Sheet.Close>
</Flex>
</Sheet.Content>Close after saving
Hold the open state to close the sheet once a request succeeds.
const [open, setOpen] = React.useState(false);
<Sheet.Root open={open} onOpenChange={setOpen}>
{/* trigger and content */}
</Sheet.Root>Props
Root
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | The open state. Controlled | |
defaultOpen | boolean | false | The open state on first render |
onOpenChange | (open: boolean) => void | Called when the sheet opens or closes |
Content
| Prop | Type | Default | Description |
|---|---|---|---|
side | 'start' | 'end' | 'top' | 'bottom' | 'left' | 'right' | 'start' | The edge it opens against |
size | '1' | '2' | '3' | '4', responsive | Theme size | Padding, and the width cap for start and end |
width | string, responsive | 90vw | The panel's width. Start and end only |
minWidth | string, responsive | The narrowest width | |
maxWidth | string, responsive | The size's width | The widest width. Overrides the size's cap |
height | string, responsive | 75vh | The panel's height. Top and bottom only |
minHeight | string, responsive | The shortest height | |
maxHeight | string, responsive | The tallest height | |
container | HTMLElement | document.body | Where the portal renders |
forceMount | boolean | Keeps the sheet in the DOM while closed | |
onEscapeKeyDown | (event) => void | Called on Escape. Call preventDefault() to keep it open | |
onPointerDownOutside | (event) => void | Called on a press outside. Call preventDefault() to keep it open |
Content also accepts every attribute of the HTML div element.
Title and Description
Title takes every Heading prop, with size="4" and weight="medium" by default. Description takes every Text prop, with size="2" and emphasis="medium" by default.
Trigger and Close
Both take one child element and pass their behaviour to it. They accept every attribute of the HTML button element.
Accessibility
| Key | Action |
|---|---|
| Escape | Closes the sheet |
| Tab | Moves focus to the next control in the sheet |
| Shift + Tab | Moves focus to the previous control in the sheet |
- Name: Give the sheet a name with
Sheet.Titleoraria-labelon Content. In development, a console warning appears when Content has neither. - Role: A screen reader announces a modal dialog. The title names it and the description describes it. Content behind it is hidden from assistive technology.
- Focus: Focus moves to the first control in the sheet when it opens, and Tab cycles inside it. Focus returns to the trigger when it closes.
- Dismissal: Escape and a press on the overlay both close the sheet.
- Direction:
startandendfollow the reading direction, so a navigation sheet opens from the right in right-to-left text.