# react-edge-sheet — Complete Documentation Source: https://react-sheet.borao.dev | npm: react-edge-sheet | GitHub: https://github.com/boraoksuzoglu/react-edge-sheet --- # Getting Started `react-edge-sheet` is a lightweight React component for sliding panels from any screen edge — top, bottom, left, or right. Zero runtime dependencies, TypeScript-first, ~4 kB gzipped. ## Installation ```bash npm install react-edge-sheet ``` Or with other package managers: ```bash yarn add react-edge-sheet pnpm add react-edge-sheet ``` ### Peer Dependencies `react-edge-sheet` requires React 17 or higher: ```bash npm install react react-dom ``` ## Your First Sheet Here's the simplest possible usage using the **imperative ref API**: ```tsx import { useRef } from 'react'; import { Sheet, SheetRef } from 'react-edge-sheet'; export function App() { const ref = useRef(null); return ( <>

Hello from the bottom!

); } ``` ## Controlled Mode Alternatively, use the **controlled API** with `open` and `onOpenChange`: ```tsx import { useState } from 'react'; import { Sheet } from 'react-edge-sheet'; export function App() { const [open, setOpen] = useState(false); return ( <>

Right Drawer

); } ``` ## Next.js Notes The `Sheet` component uses `createPortal` and `document`, which makes it incompatible with SSR. Wrap it in a dynamic import with `ssr: false`: ```tsx import dynamic from 'next/dynamic'; const Sheet = dynamic(() => import('react-edge-sheet').then((m) => ({ default: m.Sheet })), { ssr: false, }); ``` Or wrap your component using the Sheet: ```tsx const MyDrawer = dynamic(() => import('./MyDrawer'), { ssr: false }); ``` ## TypeScript Imports All types are exported from the package root: ```ts import { Sheet, SheetRef, SheetProps, SheetEdge } from 'react-edge-sheet'; ``` | Export | Description | | ------------ | ---------------------------------------- | | `Sheet` | The main component | | `SheetRef` | Type for the imperative ref handle | | `SheetProps` | Full props interface | | `SheetEdge` | `'top' \| 'bottom' \| 'left' \| 'right'` | --- # API Reference ## `` Props ### `edge` Which screen edge the sheet slides from. ```ts type SheetEdge = 'top' | 'bottom' | 'left' | 'right'; ``` Default: `'bottom'` ### `align` Position alignment when the sheet is docked to an edge: - **Top/bottom edges**: `align` controls horizontal position (`start` = left, `center` = middle, `end` = right) - **Left/right edges**: `align` controls vertical position (`start` = top, `center` = middle, `end` = bottom) Common use case: `edge="top"` + `align="end"` for a notifications dropdown in the top-right corner. ```ts type Align = 'start' | 'center' | 'end'; ``` Default: `'center'` ### `open` / `onOpenChange` Controlled mode. Pass `open` to take control of visibility state. `onOpenChange` is called when the sheet should open or close (backdrop click, Escape key). ```tsx ... ``` ### `portal` Where to render the sheet via `createPortal`. - `undefined` (default) — renders into `document.body` - `HTMLElement` — renders into a specific element - `null` — renders inline (no portal) ### `onOpen` / `onClose` Callbacks fired **after the animation completes**, not when the state changes. ```tsx console.log('fully open')} onClose={() => console.log('fully closed')}> ... ``` --- ## `SheetRef` Methods Access these via `ref.current` when using the imperative API. | Method | Signature | Description | | -------- | ------------ | ------------------------------------ | | `open` | `() => void` | Opens the sheet | | `close` | `() => void` | Closes the sheet with exit animation | | `toggle` | `() => void` | Toggles open/closed | | `isOpen` | `boolean` | Current open state | ```tsx const ref = useRef(null); // Later: ref.current?.open(); ref.current?.close(); ref.current?.toggle(); console.log(ref.current?.isOpen); ``` --- ## `SheetEdge` Type ```ts type SheetEdge = 'top' | 'bottom' | 'left' | 'right'; ``` --- ### `backdrop` / `backdropComponent` Control backdrop visibility and customization (similar to gorhom/bottom-sheet): - **`backdrop={true}`** (default): Shows the built-in backdrop. - **`backdrop={false}`**: No backdrop — sheet-only modal. - **`backdropComponent`**: Custom backdrop. Receives `{ isExiting, isEntered, close, closeOnBackdropClick, style?, className? }`. ```tsx // No backdrop ... // Custom backdrop (
)} > ... ``` --- ## Exported Components & Hooks ### `SheetBackdrop` Default backdrop component (opacity-animated, `blur(20px)` by default). Use with `backdropComponent` to customize: ```tsx import { Sheet, SheetBackdrop } from 'react-edge-sheet'; ( )} > ... ; ``` ### `useSheet` Headless hook for custom sheet UIs: ```tsx import { useSheet } from 'react-edge-sheet'; const { isOpen, isExiting, isEntered, openFn, closeFn, toggle } = useSheet({ open: controlledOpen, onOpenChange: setOpen, onOpen: () => console.log('opened'), onClose: () => console.log('closed'), }); ``` ### `useSheetContext` Access sheet state inside any component rendered as a child of `Sheet`. Useful for building custom backdrops or close buttons: ```tsx import { useSheetContext } from 'react-edge-sheet'; function MyCloseButton() { const { isEntered, isExiting, close } = useSheetContext(); return ( ); } ``` ### `SheetBackdropComponentProps` ```ts interface SheetBackdropComponentProps { isExiting: boolean; isEntered: boolean; close: () => void; closeOnBackdropClick: boolean; style?: React.CSSProperties; className?: string; } ``` --- ## Full Props Interface ```ts interface SheetProps { children?: React.ReactNode; /** Which screen edge the sheet slides from. Default: 'bottom' */ edge?: SheetEdge; /** Horizontal align for top/bottom, vertical align for left/right. Default: 'center' */ align?: 'start' | 'center' | 'end'; /** Controlled open state */ open?: boolean; /** Called when open state should change */ onOpenChange?: (open: boolean) => void; /** Fires after enter animation completes */ onOpen?: () => void; /** Fires after exit animation completes */ onClose?: () => void; /** Portal target. undefined = document.body, null = inline render */ portal?: HTMLElement | null; /** Close when backdrop is clicked. Default: true */ closeOnBackdropClick?: boolean; /** When false, no backdrop is rendered. Default: true */ backdrop?: boolean; /** Custom backdrop component. Receives SheetBackdropComponentProps */ backdropComponent?: (props: SheetBackdropComponentProps) => React.ReactNode; /** Animate panel size changes via ResizeObserver. Default: true */ animateSize?: boolean; /** CSS z-index. Default: 200 */ zIndex?: number; /** CSS class for the full-screen overlay (portal root). Not the panel. */ containerClassName?: string; /** Inline styles merged onto the overlay container */ containerStyle?: React.CSSProperties; /** max-height (vertical sides) or max-width (horizontal sides) */ maxSize?: string; /** Explicit max-height of the panel */ maxHeight?: string; /** Explicit max-width of the panel */ maxWidth?: string; /** Shorthand: min-height (vertical) or min-width (horizontal) */ minSize?: string; /** Explicit min-height of the panel */ minHeight?: string; /** Explicit min-width of the panel */ minWidth?: string; /** Override panel slide transition */ transition?: string; /** Override size-change transition when animateSize is true */ sizeTransition?: string; /** Override backdrop opacity transition (default backdrop only) */ backdropTransition?: string; /** Inline styles for the backdrop (default backdrop only) */ backdropStyle?: React.CSSProperties; /** CSS class for the backdrop (default backdrop only) */ backdropClassName?: string; /** Inline styles for the sheet panel */ style?: React.CSSProperties; /** CSS class for the sheet panel */ className?: string; /** CSS class for the animate-size inner wrapper (when animateSize and no snap points) */ innerWrapperClassName?: string; /** Inline styles for the animate-size inner wrapper */ innerWrapperStyle?: React.CSSProperties; /** CSS class for the inner content wrapper (the div that wraps children) */ contentClassName?: string; /** Inline styles for the inner content wrapper */ contentStyle?: React.CSSProperties; /** Enable drag-to-dismiss gesture. Default: false (true when showDragHandle is true) */ draggable?: boolean; /** Show a drag handle pill. Also enables draggable unless explicitly set to false. */ showDragHandle?: boolean; /** Inline styles for the default drag handle pill. Ignored when dragHandleComponent is set. */ dragHandleStyle?: React.CSSProperties; /** CSS class for the default drag handle pill. Ignored when dragHandleComponent is set. */ dragHandleClassName?: string; /** Replace the default drag handle pill with a custom element. */ dragHandleComponent?: React.ReactNode; /** Pixels to drag before dismissing. Default: 80 */ dragThreshold?: number; /** Velocity (px/ms) above which release triggers dismiss. Default: 0.3 */ dragVelocityThreshold?: number; /** Snap points array in ascending order, e.g. ['200px','50vh','90vh'] */ snapPoints?: string[]; /** Initial snap index. Default: last (largest) snap point */ defaultSnapPoint?: number; /** Called when the active snap point changes */ onSnapChange?: (index: number) => void; /** * Lock body scroll while the sheet is open. Default true. false keeps the page scrollable. */ scrollLock?: boolean; /** * Body padding during scroll lock (ignored when scrollLock is false). * true = scrollbar width (default), false = none, string = custom (e.g. "0", "1rem"). */ scrollLockPadding?: boolean | string; } ``` --- # Examples ## Bottom Sheet The most common pattern — a sheet that slides up from the bottom, floating away from the edge with fully-rounded corners: ```tsx import { useRef } from 'react'; import { Sheet, SheetRef } from 'react-edge-sheet'; export function BottomSheet() { const ref = useRef(null); return ( <>

Bottom Sheet

Slides up from the bottom edge.

); } ``` ## Right Drawer (Navigation) A sidebar navigation drawer that floats from the right edge: ```tsx import { useRef } from 'react'; import { Sheet, SheetRef } from 'react-edge-sheet'; export function RightDrawer() { const ref = useRef(null); return ( <>

Navigation

); } ``` ## Top Bar (Command Palette) A floating command palette that drops from the top (centered): ```tsx import { useRef } from 'react'; import { Sheet, SheetRef } from 'react-edge-sheet'; export function CommandPalette() { const ref = useRef(null); return ( <>
); } ``` ## Top-Right Notifications Use `align="end"` with `edge="top"` to position a notifications dropdown in the top-right corner: ```tsx import { useRef } from 'react'; import { Sheet, SheetRef } from 'react-edge-sheet'; export function NotificationsDropdown() { const ref = useRef(null); return ( <>

Notifications

  • New comment on your post
  • Deploy completed
); } ``` ## Glassmorphism Style A frosted-glass sheet with a tinted gradient backdrop: ```tsx {/* content */} ``` ## Dynamic Content Height The `animateSize` prop (enabled by default) uses a `ResizeObserver` to smoothly animate the sheet panel when its content height changes. Click the button below to open a sheet where you can add and remove items — watch the panel grow and shrink: ```tsx import { useRef, useState } from 'react'; import { Sheet, SheetRef } from 'react-edge-sheet'; const TASKS = ['Review PRs', 'Update deps', 'Write tests', 'Deploy']; export function DynamicSheet() { const ref = useRef(null); const [items, setItems] = useState(TASKS.slice(0, 2)); return ( <> {/* animateSize is true by default — ResizeObserver drives height */}

Tasks ({items.length})

{items.map((item, i) => (
{item}
))}
); } ``` The key: `animateSize` (default `true`) applies a CSS height transition driven by `ResizeObserver`. Set `animateSize={false}` to disable it. ## Controlled Mode When your parent component owns the open state: ```tsx import { useState } from 'react'; import { Sheet } from 'react-edge-sheet'; export function ControlledSheet() { const [open, setOpen] = useState(false); return ( <>

State managed externally.

); } ``` ## Custom Portal Target Render the sheet into a specific DOM element instead of `document.body`: ```tsx import { useRef, useEffect, useState } from 'react'; import { Sheet, SheetRef } from 'react-edge-sheet'; export function CustomPortal() { const ref = useRef(null); const [container, setContainer] = useState(null); useEffect(() => { setContainer(document.getElementById('sheet-container')); }, []); return ( <>
{container && (
Contained sheet!
)} ); } ``` ## No Backdrop (Sheet-Only) Use `backdrop={false}` when you don't want an overlay — useful for non-modal floating panels: ```tsx
Sheet without backdrop — background stays interactive.
``` ## Inline Render (No Portal) Pass `portal={null}` to render the sheet inline in the document flow: ```tsx
No portal — renders in place.
``` ## onOpen / onClose Callbacks Execute logic after the animation fully completes: ```tsx { // Focus first input, fetch data, etc. inputRef.current?.focus(); }} onClose={() => { // Clean up, reset form state, etc. resetForm(); }} >
...
``` --- # Advanced ## Animation Internals `react-edge-sheet` uses CSS `transform` transitions for animations — no JavaScript animation libraries, no `requestAnimationFrame` loops. For animation presets and asymmetric enter/exit transitions, see the [Animations guide](/docs/animation). ### How the enter animation works 1. Sheet mounts with the panel off-screen (e.g. `translateY(150%)` for bottom) 2. A `requestAnimationFrame` call triggers `setIsEntered(true)` — giving the browser one paint to register the off-screen position 3. CSS transition runs: `transform` changes to `translateY(0)` over `300ms` The 150% offset (not 100%) prevents a visible pixel flash on some devices when the panel first appears. ### Exit animation 1. `close()` sets `isExiting = true` 2. Transform transitions back to the off-screen value 3. The CSS `transitionend` event fires (guarded to only run on `transform` on the panel element) 4. Internal state resets — `isExiting` and `isOpen` both become false — unmounting the panel --- ## Size Animation When `animateSize={true}` (default), the panel smoothly resizes when its content changes height or width. This uses a `ResizeObserver` on the inner content div. ```tsx // Disable size animation for static content: ... ``` The animated size transition uses: - `height` (vertical sides: top/bottom) - `width` (horizontal sides: left/right) with a `0.25s ease` transition. --- ## Custom Styling The library exposes four style layers (outside → inside): 1. **Overlay** — `containerClassName` / `containerStyle`: full-screen portal root (backdrop + panel). Merged with defaults (`position: fixed`, `inset: 0`, flex alignment, `zIndex`). Overriding `position` / `inset` may break the overlay. 2. **Panel** — `className` / `style`: the dialog panel (`role="dialog"`). 3. **Animate-size wrapper** — `innerWrapperClassName` / `innerWrapperStyle`: only when `animateSize` is true and `snapPoints` is not used. Overriding `height`, `width`, or `overflow` may break size animation. 4. **Content** — `contentClassName` / `contentStyle`: innermost wrapper around children (ResizeObserver target). ### Style the sheet panel Use the `style` prop for inline styles on the panel element: ```tsx ... ``` ### Style with className Use the `className` prop with your CSS framework: ```tsx // Tailwind CSS ... ``` ### Content wrapper (contentClassName, contentStyle) The sheet wraps your children in an inner div used for measuring (ResizeObserver). Use `contentClassName` and `contentStyle` to style this wrapper — e.g. for overflow, scroll, or padding: ```tsx ... ``` ### Position alignment The `align` prop positions the sheet along the edge: - **Top/bottom**: `start` (left), `center` (default), `end` (right) - **Left/right**: `start` (top), `center` (default), `end` (bottom) ```tsx // Notifications dropdown in top-right corner ... // Sidebar anchored to bottom ... ``` ### Limit panel size `maxSize`, `maxHeight`, and `maxWidth` are optional. When omitted, no max constraint is applied — the panel size is determined by content and parent layout. Use `maxSize` for shorthand (max-height for top/bottom, max-width for left/right): ```tsx ... ... ``` ### Backdrop customization **Backdrop blur**: Use `backdropStyle` to override the default `blur(20px)`: ```tsx ... // lighter ... // no blur ... // heavy ``` **No backdrop** (sheet-only modal): ```tsx ... ``` **Custom backdrop** (like gorhom/bottom-sheet): Use `backdropComponent` to render your own overlay: ```tsx (
)} > ... ``` ### Min / max size Use `minSize`, `minHeight`, `minWidth` for minimum dimensions (mirrors the max API): ```tsx ... ... ``` ### Scroll lock By default, opening the sheet locks body scroll (`overflow: hidden`). Disable this entirely with `scrollLock={false}` so the page stays scrollable behind the sheet: ```tsx ... ``` When scroll lock is enabled, `padding-right` is applied by default to match the scrollbar width and avoid layout shift. Control padding only (not the lock itself) with `scrollLockPadding`: ```tsx // Lock scroll but skip padding (may cause layout shift when scrollbar hides) ... // Custom padding while locked ... ``` `scrollLockPadding` has no effect when `scrollLock` is `false`. ### Transition overrides Override the built-in transitions via props: ```tsx ... ``` --- ## Accessibility The sheet panel is rendered with `role="dialog"` and `aria-modal="true"`. Escape key always closes the sheet, and a focus trap keeps keyboard navigation inside the panel while it is open. For full details — ARIA props, focus restoration, screen reader guidance — see the [Keyboard & Focus guide](/docs/keyboard). Quick example: ```tsx

Sheet Title

Description of the sheet content.

``` --- ## SSR / Next.js `Sheet` uses `createPortal` and accesses `document`, which makes it incompatible with SSR. Two patterns work: ### Pattern 1: Wrap the component using Sheet ```tsx // MyDrawer.tsx import { useRef } from 'react'; import { Sheet, SheetRef } from 'react-edge-sheet'; export default function MyDrawer() { const ref = useRef(null); // ... } // page.tsx import dynamic from 'next/dynamic'; const MyDrawer = dynamic(() => import('./MyDrawer'), { ssr: false }); ``` ### Pattern 2: Dynamic import of Sheet itself ```tsx import dynamic from 'next/dynamic'; const Sheet = dynamic(() => import('react-edge-sheet').then((m) => ({ default: m.Sheet })), { ssr: false, }); ``` ### Remix In Remix, use `ClientOnly` from `remix-utils`: ```tsx import { ClientOnly } from 'remix-utils/client-only'; {() => }; ``` --- ## onOpen / onClose Timing `onOpen` fires **after** the enter animation completes. `onClose` fires **after** the exit animation completes — this is when the component unmounts. If you need to react to the open/close _intent_ (before animation), use `onOpenChange`: ```tsx { // fires immediately when user closes (backdrop click / Escape) if (!nextOpen) analytics.track('sheet_dismissed'); setOpen(nextOpen); }} onOpen={() => { // fires after enter animation fetchData(); }} onClose={() => { // fires after exit animation — component is about to unmount cleanup(); }} > ... ``` --- # Keyboard & Focus `react-edge-sheet` ships with a complete focus management implementation — zero configuration required. --- ## Focus Trap When a sheet opens, Tab and Shift+Tab are automatically constrained to focusable elements inside the panel. Open the sheet below and press Tab to see the cycling in action: This works automatically — no props needed: ```tsx
``` Focusable elements include: links (`a[href]`), enabled buttons, inputs, selects, textareas, elements with `tabindex` ≥ 0, and `details > summary`. Elements inside `[aria-hidden="true"]` containers are excluded. If the sheet has no focusable children, Tab focus cycles through the panel container itself. --- ## Auto-focus on Open After the enter animation completes, the first focusable element inside the sheet receives focus automatically. If no focusable elements exist, the panel itself receives focus (via `tabIndex={-1}`) so keyboard users are never stranded outside the dialog. This matches the WAI-ARIA dialog pattern. To override which element gets focus, use `onOpen`: ```tsx const closeButtonRef = useRef(null); closeButtonRef.current?.focus()}>

Content here

; ``` --- ## Focus Restoration When the sheet closes (after the exit animation), focus returns to whichever element was focused before the sheet opened. This is important for keyboard users navigating with Tab. No configuration needed — it is always on. --- ## ARIA Props The dialog element always has `role="dialog"` and `aria-modal="true"`. To make it properly labeled for screen readers, use one of the three ARIA props below — select a mode to see how each one is applied on the dialog element: ### aria-labelledby Point to the heading inside the sheet: ```tsx

Confirm deletion

This action cannot be undone.

``` ### aria-describedby Add a description for complex dialogs: ```tsx

Notification Settings

Choose how you want to receive notifications.

{/* form controls */}
``` ### aria-label Use when there is no visible heading: ```tsx ``` --- ## Escape Key Pressing Escape always closes the sheet. This is built-in and cannot be disabled — it is required by the WAI-ARIA dialog pattern. --- ## Screen Reader Guidance - Use `aria-labelledby` pointing to a visible heading whenever possible — this is preferred over `aria-label` - The sheet is announced as a dialog with `aria-modal="true"`, which tells screen readers to ignore content behind it - Place the primary action button first in DOM order for ease of access - Avoid nesting interactive sheets — screen readers may lose context --- ## Example: Accessible Confirmation Dialog ```tsx import { useRef } from 'react'; import { Sheet, SheetRef } from 'react-edge-sheet'; export function DeleteConfirmSheet({ onConfirm }: { onConfirm: () => void }) { const ref = useRef(null); return ( <>

Delete item?

This will permanently delete the item. This action cannot be undone.

); } ``` --- # Animations `react-edge-sheet` gives you multiple layers of animation control — from quick preset names to full CSS transition strings. --- ## Animation Presets Use `animationPreset` to choose from five built-in presets. Pick one below and open the sheet to feel the difference: | Preset | Transition | Character | | --------- | ---------------------------------------------- | --------------------- | | `default` | `0.42s cubic-bezier(0.4, 0, 0.2, 1)` | Smooth material-style | | `spring` | `0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)` | Slight overshoot | | `bounce` | `0.6s cubic-bezier(0.34, 1.56, 0.64, 1)` | Pronounced bounce | | `snappy` | `0.22s cubic-bezier(0.4, 0, 0.2, 1)` | Fast and crisp | | `slow` | `0.7s cubic-bezier(0.4, 0, 0.2, 1)` | Deliberate, cinematic | ```tsx // Spring — slight overshoot, feels alive ... // Snappy — fast UI panels ... // Bounce — playful drawers ... ``` --- ## Asymmetric Enter / Exit Transitions Use `enterTransition` and `exitTransition` to set different animations for opening and closing. Select a mode below — open and close slowly to notice the difference: ```tsx // Fast in, slow out ... ``` ```tsx // Instant open, animated close (command palette style) ... ``` --- ## Priority Order Transitions are resolved with the following priority (highest wins): 1. `transition` — full override, applies to both enter and exit 2. `enterTransition` / `exitTransition` — directional overrides 3. `animationPreset` — named preset 4. Built-in default (`default` preset) ```tsx // transition overrides everything {/* uses 0.25s ease-out, bounce is ignored */} // enterTransition takes priority over preset for enter only {/* enter: 0.1s ease | exit: slow preset (0.7s) */} ``` --- ## Size Transition Control the smooth resize animation when content height or width changes (`animateSize={true}`): ```tsx ... ``` Disable size animation entirely: ```tsx ... ``` --- ## Backdrop Transition Override the backdrop fade animation (default backdrop only): ```tsx ... ``` --- ## Full Override `transition` replaces the built-in transition completely with any CSS transition string: ```tsx ... ``` --- ## Example: Notification Drawer ```tsx // Snappy enter, gentle exit — ideal for transient panels

Your file has been saved.

``` --- # Gestures `react-edge-sheet` supports touch and mouse drag gestures for dismissing sheets and snapping between sizes. --- ## Drag to Dismiss Add `draggable` to enable the drag-to-dismiss gesture. The demo below uses `showDragHandle` — grab the pill and drag down: ```tsx

Drag down to close

``` Works on all edges — drag down for `bottom`, drag up for `top`, drag left for `left`, drag right for `right`. ### Drag Handle Add a visible handle pill with `showDragHandle`. This also enables `draggable` automatically: ```tsx

Content here

``` The handle is positioned relative to the edge: - `bottom` → top-center, horizontal pill - `top` → bottom-center, horizontal pill - `left` → right-center, vertical pill - `right` → left-center, vertical pill To show the handle without enabling drag: ```tsx ... ``` ### Drag Handle Customization **Style the default pill** with `dragHandleStyle` / `dragHandleClassName`: ```tsx ... ``` **Replace it entirely** with `dragHandleComponent`. `showDragHandle` is not required — providing `dragHandleComponent` is enough to render it: ```tsx } > ... ``` When `dragHandleComponent` is set, `dragHandleStyle` and `dragHandleClassName` are ignored. --- ## Drag Thresholds Control when a release triggers dismiss: ```tsx ... ``` A release triggers dismiss if **either** condition is met: - `|dragOffset| > dragThreshold` - `velocity > dragVelocityThreshold` For a forgiving feel (easier to dismiss), lower both values. For a sticky feel (harder to dismiss), raise them. --- ## Rubber Band Dragging against the dismiss direction (e.g. dragging up on a bottom sheet) creates a rubber band resistance — the offset is 20% of actual drag distance. This gives natural physical feedback. --- ## Snap Points Use `snapPoints` to define multiple heights (or widths for left/right edges). The demo below has three snap heights — drag up/down to cycle between them: ```tsx

Drag to snap between heights

``` Snap points should be in **ascending order** (smallest to largest). ### Default Snap Point By default the sheet opens at the largest snap point (last index). Override with `defaultSnapPoint`: ```tsx ... ``` ### onSnapChange React to snap changes with `onSnapChange`: ```tsx console.log('snapped to', index)} > ... ``` ### Collapsing from the smallest snap Dragging toward dismiss from the smallest snap point (`index === 0`) closes the sheet entirely. --- ## Common Patterns ### Mini / Half / Full Sheet ```tsx

Title

Your content here.

``` ### Map-Style Sheet ```tsx

Nearby Places

{/* list items */}
``` ### Music Player Snap Drag up to expand to full player, drag down to collapse to mini player. Drag below mini to close. ```tsx
{/* mini player or full player depending on snap */}
``` ### Sidebar with Drag ```tsx ``` --- # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- ## [0.5.0] — 2026-03-24 ### Added - `containerClassName` and `containerStyle`: customize the full-screen overlay container (portal root that wraps backdrop and panel). - `innerWrapperClassName` and `innerWrapperStyle`: customize the animate-size inner wrapper (only when `animateSize` is true and `snapPoints` is not set). ## [0.4.0] — 2026-03-17 ### Added - `scrollLock` prop: when `false`, disables body scroll lock entirely so the page stays scrollable while the sheet is open. Default remains `true`. `scrollLockPadding` only applies when `scrollLock` is `true`. ## [0.3.0] — 2026-03-16 ### Added - `contentClassName` and `contentStyle` props: style the inner content wrapper (the div that wraps children, used for ResizeObserver). Useful for overflow, scroll, padding, and other layout tweaks. ## [0.2.0] — 2026-03-13 ### Added - `scrollLockPadding` prop: control body padding during scroll lock. `true` (default) uses scrollbar width, `false` disables padding, `string` for custom value (e.g. `"0"`, `"1rem"`). ### Changed - **Breaking:** Removed default `maxHeight: 90vh` and `maxWidth: 90vw`. When `maxHeight`/`maxWidth`/`maxSize` are not provided, no max constraint is applied — size is determined by content and parent layout. ## [0.1.1] — 2026-03-07 ### Changed - Code formatting (spacing, tabs, Prettier) ## [0.1.0] — 2026-03-07 ### Initial release - Initial release of the project