Sheet

Edge drawer built on the native dialog element.

organismsSource
import { Sheet, SheetTrigger, SheetHeader, SheetTitle, SheetDescription, SheetFooter, SheetClose, SheetContent } from '@elirobinson/react/components/organisms/Sheet';

Styles: @elirobinson/react/styles/organisms/Sheet.css — already included when you import @elirobinson/react/styles.css.

Filter coaching guides

Narrow the list by sport, age group, or session type.

Show code
import { useState } from 'react';
import { Button } from '@elirobinson/react/components/atoms/Button';
import {
  Sheet,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from '@elirobinson/react/components/organisms/Sheet';

export default function Basic() {
  const [open, setOpen] = useState(false);

  return (
    <Sheet open={open} onOpenChange={setOpen}>
      <SheetTrigger className="ds-button ds-button--primary">Filter guides</SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Filter coaching guides</SheetTitle>
          <SheetDescription>Narrow the list by sport, age group, or session type.</SheetDescription>
        </SheetHeader>
        <SheetFooter>
          <SheetClose />
          <Button variant="accent" onClick={() => setOpen(false)}>
            Apply filters
          </Button>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  );
}

When to use it

Use Sheet for a task that needs more room than a Popover but shouldn't take over the whole screen — filters, a detail panel, a settings drawer. It's built on the native <dialog> element, so the focus trap, the backdrop, and Escape-to-close all come from the browser instead of a hand-rolled implementation.

Reach for Dialog instead when the content itself is the task and centering it is the point — a confirmation, a form that should block the rest of the page. A Sheet stays anchored to an edge, which reads as "secondary to what's behind it" even while it's open.

Sides

SheetContent takes a sideleft, right, top, or bottom. right is the default, shown above.

Opens from the left

Set side="left" on SheetContent to slide in from this edge.

Opens from the right

Set side="right" on SheetContent to slide in from this edge.

Opens from the top

Set side="top" on SheetContent to slide in from this edge.

Opens from the bottom

Set side="bottom" on SheetContent to slide in from this edge.

Show code
import { useState } from 'react';
import {
  Sheet,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from '@elirobinson/react/components/organisms/Sheet';

const SIDES = ['left', 'right', 'top', 'bottom'] as const;

function SideSheet({ side }: { side: (typeof SIDES)[number] }) {
  const [open, setOpen] = useState(false);
  const label = side.charAt(0).toUpperCase() + side.slice(1);

  return (
    <Sheet open={open} onOpenChange={setOpen}>
      <SheetTrigger className="ds-button ds-button--secondary">{label}</SheetTrigger>
      <SheetContent side={side}>
        <SheetHeader>
          <SheetTitle>Opens from the {side}</SheetTitle>
          <SheetDescription>
            Set side=&quot;{side}&quot; on SheetContent to slide in from this edge.
          </SheetDescription>
        </SheetHeader>
        <SheetFooter>
          <SheetClose />
        </SheetFooter>
      </SheetContent>
    </Sheet>
  );
}

export default function Sides() {
  return (
    <div className="demo-row">
      {SIDES.map((side) => (
        <SideSheet key={side} side={side} />
      ))}
    </div>
  );
}

Props

PropTypeDefaultDescription
defaultOpenbooleanfalse
onOpenChange((open: boolean) => void)
openboolean

SheetTrigger

No props of its own beyond the inherited HTML attributes.

Also accepts all HTMLAttributes<HTMLButtonElement> props.

SheetHeader

No props of its own beyond the inherited HTML attributes.

Also accepts all HTMLAttributes<HTMLDivElement> props.

SheetTitle

No props of its own beyond the inherited HTML attributes.

Also accepts all HTMLAttributes<HTMLHeadingElement> props.

SheetDescription

No props of its own beyond the inherited HTML attributes.

Also accepts all HTMLAttributes<HTMLParagraphElement> props.

SheetFooter

No props of its own beyond the inherited HTML attributes.

Also accepts all HTMLAttributes<HTMLDivElement> props.

SheetClose

No props of its own beyond the inherited HTML attributes.

Also accepts all HTMLAttributes<HTMLButtonElement> props.

SheetContent

PropTypeDefaultDescription
side"left" | "right" | "top" | "bottom"right

Also accepts all HTMLAttributes<HTMLDialogElement> props.

Accessibility

  • SheetContent calls the native showModal() when it opens, so it inherits the browser's own focus trap, ::backdrop, and background inerting — none of that is reimplemented.
  • Keyboard: Escape closes the sheet. That's native <dialog> behavior (it fires a cancel then close event), which Sheet listens for and turns into onOpenChange(false) — no keydown handler needed.
  • Clicking the backdrop — the <dialog> element itself, outside the inner content — also closes the sheet.
  • SheetTitle and SheetDescription supply the ids SheetContent points aria-labelledby and aria-describedby at. Always render a SheetTitle, even visually hidden — the reference is unconditional, so a sheet without one is a dialog with no accessible name.
  • SheetClose renders a real <button type="button"> with a "Close" fallback label when you don't pass children.

Do

  • Render a SheetTitle inside SheetHeader every time, even if you hide it visually.
  • Drive visibility with the open/onOpenChange controlled pair.
  • Pick side to match the content — top or bottom for something short and wide, left or right for anything list-like.
  • Pair SheetClose with an explicit confirm action in SheetFooter so Cancel and the primary action are both one click away.

Don't

  • Mix defaultOpen with the controlled open prop — pick uncontrolled (defaultOpen) or controlled (open/onOpenChange), not both.
  • Nest a second Sheet inside the first one's content — close the first before opening another.
  • Skip SheetDescription and expect the title alone to explain what the sheet is for.
  • Put a primary action only inside the sheet with no way to reach it by keyboard — it inherits full native dialog tabbing, so this is rarely an issue unless you fight it with tabIndex.