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.

SideOpens againstUse it for
startThe leading edge, left in left-to-right textNavigation
endThe trailing edge, right in left-to-right textDetails, inspectors and edit forms
topThe top edgeContent tied to a top bar, such as search
bottomThe bottom edgeActions 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

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

PropTypeDefaultDescription
openbooleanThe open state. Controlled
defaultOpenbooleanfalseThe open state on first render
onOpenChange(open: boolean) => voidCalled when the sheet opens or closes

Content

PropTypeDefaultDescription
side'start' | 'end' | 'top' | 'bottom' | 'left' | 'right''start'The edge it opens against
size'1' | '2' | '3' | '4', responsiveTheme sizePadding, and the width cap for start and end
widthstring, responsive90vwThe panel's width. Start and end only
minWidthstring, responsiveThe narrowest width
maxWidthstring, responsiveThe size's widthThe widest width. Overrides the size's cap
heightstring, responsive75vhThe panel's height. Top and bottom only
minHeightstring, responsiveThe shortest height
maxHeightstring, responsiveThe tallest height
containerHTMLElementdocument.bodyWhere the portal renders
forceMountbooleanKeeps the sheet in the DOM while closed
onEscapeKeyDown(event) => voidCalled on Escape. Call preventDefault() to keep it open
onPointerDownOutside(event) => voidCalled 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

KeyAction
EscapeCloses the sheet
TabMoves focus to the next control in the sheet
Shift + TabMoves focus to the previous control in the sheet
  • Name: Give the sheet a name with Sheet.Title or aria-label on 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: start and end follow the reading direction, so a navigation sheet opens from the right in right-to-left text.