react ~6 KB 0 deps v0.1.1 ↗ GitHub ↗

@mshafiqyajid/react-sheet

Headless hook and styled sheet component for React. Bottom, top, left, and right positions, snap points, swipe-to-dismiss, focus trap, body scroll lock, Escape to close, and smooth spring slide animations. Zero dependencies.

Playground #

Props
TSX
import { SheetStyled } from "@mshafiqyajid/react-sheet/styled";
import "@mshafiqyajid/react-sheet/styles.css";

<SheetStyled
  open={open}
  onOpenChange={setOpen}
  showTitle
  showFooter
/>

Install #

npm install @mshafiqyajid/react-sheet

Quick start #

import { useState } from "react";
import { SheetStyled } from "@mshafiqyajid/react-sheet/styled";
import "@mshafiqyajid/react-sheet/styles.css";

const [open, setOpen] = useState(false);

<button onClick={() => setOpen(true)}>Open sheet</button>

<SheetStyled
  open={open}
  onOpenChange={setOpen}
  title="Options"
  footer={<button onClick={() => setOpen(false)}>Done</button>}
>
  <p>Sheet content goes here.</p>
</SheetStyled>

Side variants #

Set side to control which edge the sheet slides in from. Default is "bottom".

<SheetStyled open={open} onOpenChange={setOpen} side="right" title="Settings">
  <p>Slides in from the right.</p>
</SheetStyled>

<SheetStyled open={open} onOpenChange={setOpen} side="top" title="Notifications">
  <p>Slides down from the top.</p>
</SheetStyled>

<SheetStyled open={open} onOpenChange={setOpen} side="left" title="Navigation">
  <p>Slides in from the left.</p>
</SheetStyled>

Snap points #

Pass snapPoints to allow the sheet to snap to intermediate heights. Drag past the last snap point to dismiss.

<SheetStyled
  open={open}
  onOpenChange={setOpen}
  snapPoints={["40vh", "80vh"]}
  defaultSnapPoint="40vh"
  onSnapPointChange={(snap) => console.log("snapped to", snap)}
>
  <p>Drag up to expand to 80 vh, drag past threshold to dismiss.</p>
</SheetStyled>

Swipe-to-dismiss #

swipeToDismiss is enabled by default. Set a custom swipeThreshold (pixels) if needed.

<SheetStyled
  open={open}
  onOpenChange={setOpen}
  swipeToDismiss={true}
  swipeThreshold={80}
>
  <p>Drag more than 80 px to dismiss.</p>
</SheetStyled>

{/* Disable swipe */}
<SheetStyled open={open} onOpenChange={setOpen} swipeToDismiss={false}>
  <p>Swipe disabled — use the overlay or Escape to close.</p>
</SheetStyled>

Focus management #

Focus is trapped inside the sheet while open and returned to the previously-focused element on close. Override with initialFocusRef and finalFocusRef.

const inputRef = useRef<HTMLInputElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);

<button ref={triggerRef} onClick={() => setOpen(true)}>Open</button>

<SheetStyled
  open={open}
  onOpenChange={setOpen}
  initialFocusRef={inputRef}
  finalFocusRef={triggerRef}
>
  <input ref={inputRef} placeholder="Focused on open" />
</SheetStyled>

Lifecycle callbacks #

<SheetStyled
  open={open}
  onOpenChange={setOpen}
  onAfterOpen={() => console.log("sheet fully open")}
  onAfterClose={() => console.log("sheet fully closed")}
>
  <p>Content</p>
</SheetStyled>

Headless #

import { useSheet } from "@mshafiqyajid/react-sheet";

const { isOpen, open, close, sheetProps, overlayProps, handleProps, dragY } = useSheet({
  swipeToDismiss: true,
  swipeThreshold: 60,
});

<button onClick={open}>Open</button>

{isOpen && (
  <div {...overlayProps} style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.4)" }}>
    <div
      {...sheetProps}
      style={{
        position: "fixed",
        bottom: 0, left: 0, right: 0,
        background: "#fff",
        borderRadius: "16px 16px 0 0",
        padding: "1rem",
        transform: dragY > 0 ? `translateY(${dragY}px)` : undefined,
      }}
    >
      <div {...handleProps} style={{ width: 40, height: 5, background: "#ccc", borderRadius: 9999, margin: "0 auto 1rem" }} />
      <p>Headless sheet with drag tracking.</p>
      <button onClick={close}>Close</button>
    </div>
  </div>
)}

API #

PropTypeDefaultDescription
openbooleanControlled open state
defaultOpenbooleanfalseUncontrolled default open state
onOpenChange(open: boolean) => voidCalled when open state changes
onAfterOpen() => voidFires after the open transition completes
onAfterClose() => voidFires after the close transition completes
side"bottom" | "top" | "left" | "right""bottom"Which edge the sheet slides from
snapPoints(string | number)[]Snap heights/widths e.g. ["40vh", "80vh"]
defaultSnapPointstring | numberInitial snap point value
onSnapPointChange(snap: string | number) => voidCalled when the active snap point changes
swipeToDismissbooleantrueAllow drag-to-dismiss
swipeThresholdnumber50Pixel drag distance required to dismiss
closeOnOverlayClickbooleantrueClose when clicking the overlay backdrop
closeOnEscbooleantrueClose on Escape key
showHandlebooleantrueShow the drag handle pill (hidden on left/right)
titleReactNodeSheet heading — omit to hide the header
descriptionReactNodeAccessible description linked via aria-describedby
footerReactNodeFooter content — omit to hide footer
childrenReactNodeScrollable body content
initialFocusRefRefObject<HTMLElement>Element to focus when sheet opens
finalFocusRefRefObject<HTMLElement>Element to focus when sheet closes
lockBodyScrollbooleantrueLock body scroll while sheet is open
containerHTMLElement | nulldocument.bodyOverride the portal container
classNamestringExtra class on the panel element
styleCSSPropertiesInline style on the panel element
refforwardedForwarded to the panel div

Data attributes

AttributeValuesDescription
data-side"bottom" | "top" | "left" | "right"Current side — on both overlay and panel
data-state"open" | "closed"Visibility state — on both overlay and panel
data-swiping"true" | undefinedPresent on panel while pointer drag is active

useSheet options & return

KeyTypeDescription
openbooleanControlled open state
defaultOpenbooleanUncontrolled default (false)
onOpenChange(open: boolean) => voidOpen change callback
closeOnEscbooleanClose on Escape (true)
swipeToDismissbooleanTrack pointer drag (true)
swipeThresholdnumberDismiss threshold in px (50)
lockBodyScrollbooleanLock body scroll (true)
Returns:
isOpenbooleanCurrent open state
open() => voidOpen the sheet
close() => voidClose the sheet
toggle() => voidToggle open/closed
sheetPropsSheetPropsSpread onto panel element (role, aria-*, tabIndex, id)
overlayPropsOverlayPropsSpread onto overlay element
handlePropsHandlePropsSpread onto drag handle element
dragYnumberCurrent drag offset in pixels (0 when idle)
Edit this page on GitHub