Skip to content

OverlaysSheet

Sheet

A non-modal panel that slides in from the right or bottom. Used for the search palette, the mobile nav, and the case study figure lightbox.

Right or bottomNot modalFocus-trap

Demo

The default is a 32 rem wide sheet that slides in from the right. On mobile, sheets that aren't "right" become "bottom" by default.

Right

Search docs

Sizes

Sizes
sm — 24 rem
md — 32 rem (default)
lg — 48 rem
full — 100% of the side

Anatomy

  • Slide-in: 250 ms ease-out (right) or 250 ms ease-out (bottom, + 12 px rise)
  • Surface: --surface-elevated with elevation-3 shadow
  • Padding: 20 px
  • Title row: 0.875 rem weight 500, with a close button on the right
  • Backdrop: 30% ink at 30% opacity; click closes the sheet
  • Body scroll on the page is NOT locked (sheets are non-modal); focus is trapped within the sheet while open

Implementation

sheet.tsx
"use client"

import { useEffect, type ReactNode } from "react"
import { X } from "lucide-react"
import SiteIcon from "@/components/site-icon"

interface SheetProps {
  open: boolean
  onClose: () => void
  side?: "right" | "bottom"
  title: string
  size?: "sm" | "md" | "lg" | "full"
  children: ReactNode
}

export function Sheet({ open, onClose, side = "right", title, size = "md", children }: SheetProps) {
  useEffect(() => {
    if (!open) return
    function onKey(e: KeyboardEvent) { if (e.key === "Escape") onClose() }
    window.addEventListener("keydown", onKey)
    return () => window.removeEventListener("keydown", onKey)
  }, [open, onClose])

  if (!open) return null
  return (
    <div className="sheet__backdrop" onClick={onClose}>
      <aside
        className={`sheet sheet--${side} sheet--${size}`}
        role="dialog"
        aria-modal="true"
        aria-label={title}
        onClick={(e) => e.stopPropagation()}
      >
        <div className="sheet__head">
          <p className="sheet__title">{title}</p>
          <button onClick={onClose} aria-label="Close">
            <SiteIcon icon={X} size="sm" tone="muted" />
          </button>
        </div>
        <div className="sheet__body">{children}</div>
      </aside>
    </div>
  )
}

Props

PropTypeDefaultDescription
openbooleanNoneRequired: controlled open state
onClose() => voidNoneRequired: close handler
side"right" | "bottom""right"Slide-in direction
titlestringNoneRequired: sheet title
size"sm" | "md" | "lg" | "full""md"Width (right) or height (bottom)