Unified overlay components for React: BottomSheet with snap points and Modal with animations
npm install @octavian-tocan/react-overlayUnified overlay components for React: BottomSheet with snap points and Modal with animations.


``bash`
npm install @octavian-tocan/react-overlayor
pnpm add @octavian-tocan/react-overlayor
yarn add @octavian-tocan/react-overlay
`bash`
npm install react react-dom motion clsx tailwind-mergeOptional: lucide-react (for DismissButton icon)
npm install lucide-react
`tsx
import { useState } from "react";
import { BottomSheet } from "@octavian-tocan/react-overlay";
export function MyBottomSheet() {
const [open, setOpen] = useState(false);
return (
<>
onDismiss={() => setOpen(false)}
snapPoints={({ maxHeight }) => [200, maxHeight 0.5, maxHeight 0.9]}
>
Drag to resize or swipe down to dismiss.
$3
`tsx
import { useState } from "react";
import { Modal, ModalHeader, ModalDescription } from "@octavian-tocan/react-overlay";
import { AlertCircle } from "lucide-react";export function MyModal() {
const [open, setOpen] = useState(false);
return (
<>
setOpen(false)} size="md">
} title="Confirm Action" />
Are you sure you want to proceed?
onClick={() => {
/ handle confirm / setOpen(false);
}}
>
Confirm
>
);
}
`Documentation
- Live Storybook
- API Reference
- Examples
- Architecture
- Migration Guide
Components
$3
A draggable bottom sheet with snap points, spring animations, and swipe-to-dismiss.
`tsx
import { BottomSheet } from "@octavian-tocan/react-overlay";function App() {
const [open, setOpen] = useState(false);
return (
open={open}
onDismiss={() => setOpen(false)}
snapPoints={({ maxHeight }) => [200, maxHeight 0.5, maxHeight 0.9]}
>
Sheet content here
);
}
`#### BottomSheet Props
| Prop | Type | Default | Description |
| --------------- | ---------------------- | ------------ | ------------------------------------ |
|
open | boolean | required | Whether the sheet is open |
| onDismiss | () => void | required | Called when sheet is dismissed |
| children | ReactNode | required | Content to render |
| snapPoints | number[] \| Function | [40%, 85%] | Snap points for the sheet |
| defaultSnap | number \| Function | last point | Initial snap point |
| header | ReactNode | - | Header content above the scroll area |
| stickyHeader | ReactNode | - | Sticky header inside the scroll area |
| footer | ReactNode | - | Sticky footer content |
| scrollLocking | boolean | true | Lock body scroll when open |
| testId | string | - | Test ID for testing |#### BottomSheet Styling Props
| Prop | Type | Default | Description |
| ------------------ | ------------------- | ------- | ------------------------------------- |
|
className | string | - | CSS class for root overlay |
| style | CSSProperties | - | Inline styles for root overlay |
| sheetClassName | string | - | CSS class for sheet container |
| sheetStyle | CSSProperties | - | Inline styles for sheet container |
| handleClassName | string | - | CSS class for handle zone |
| handleStyle | CSSProperties | - | Inline styles for handle pill |
| contentClassName | string | - | CSS class for scrollable content area |
| contentStyle | CSSProperties | - | Inline styles for scrollable content |
| unstyled | boolean \| object | - | Remove default styling (see below) |#### Unstyled Mode
The
unstyled prop removes default backgrounds and padding for full customization:`tsx
// Remove all default styling
... // Selectively remove styling
...
`| Option | Effect |
| --------- | ----------------------------------- |
|
sheet | Removes white background from sheet |
| content | Removes padding from content area |
| handle | Hides the drag handle pill |#### Gradient Background Example
`tsx
open={open}
onDismiss={() => setOpen(false)}
unstyled={{ sheet: true, content: true }}
sheetClassName="bg-gradient-to-b from-sky-400 to-sky-600"
contentClassName="p-6"
>
Sign in to continue
Create an account to get started
`#### Dark Theme with Custom Handle and Dismiss Button
For dark-themed sheets, use
handleStyle to customize the handle pill color and the unstyled dismiss button variant:`tsx
open={open}
onDismiss={() => setOpen(false)}
unstyled={{ sheet: true, content: true }}
sheetClassName="bg-gradient-to-br from-gray-900 to-gray-800"
handleStyle={{ backgroundColor: "rgba(255, 255, 255, 0.5)" }}
dismissButton={{
show: true,
position: "right",
props: { variant: "unstyled", className: "text-white hover:text-white/80" },
}}
>
Dark Theme Sheet
Custom handle and dismiss button styling
`$3
A centered modal dialog with size presets and Motion animations.
`tsx
import { Modal, ModalHeader, ModalDescription } from "@octavian-tocan/react-overlay";
import { AlertCircle } from "lucide-react";function App() {
const [open, setOpen] = useState(false);
return (
setOpen(false)} size="md">
} title="Confirm Action" />
Are you sure you want to proceed?
{/ Your content /}
);
}
`#### Modal Props
| Prop | Type | Default | Description |
| --------------------- | ---------------------------------------- | -------- | ------------------------------ |
|
open | boolean | required | Whether the modal is open |
| onDismiss | () => void | required | Called when modal is dismissed |
| children | ReactNode | required | Content to render |
| size | 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full' | 'md' | Size preset |
| padding | boolean | true | Add default padding |
| closeOnOverlayClick | boolean | true | Close on backdrop click |
| closeOnEscape | boolean | true | Close on Escape key |
| showDismissButton | boolean | true | Show X button |
| testId | string | - | Test ID for testing |$3
Low-level modal component for full customization.
`tsx
import { ModalWrapper } from "@octavian-tocan/react-overlay"; open={isOpen}
onDismiss={handleClose}
contentClassName="bg-white rounded-xl p-8 max-w-lg"
showDismissButton
>
{children}
;
`$3
#### ModalHeader
Header with icon badge and title.
`tsx
import { ModalHeader } from "@octavian-tocan/react-overlay";
import { AlertCircle } from "lucide-react"; } title="Confirm Delete" />;
`#### ModalDescription
Styled description text.
`tsx
import { ModalDescription } from "@octavian-tocan/react-overlay";This action cannot be undone. ;
`#### DismissButton
Close button for overlays.
`tsx
import { DismissButton } from "@octavian-tocan/react-overlay"; onClick={handleClose}
variant="default" // "default", "subtle", or "unstyled"
position="absolute top-3 right-3"
/>;
`| Variant | Description |
| ---------- | -------------------------------------------------------- |
|
default | White background, border, shadow - for light backgrounds |
| subtle | White background, no border - for banners |
| unstyled | No background/border/shadow - for dark backgrounds |Hooks
$3
Lock body scroll when an overlay is open. Supports multiple concurrent overlays via ref-counting.
`tsx
import { useBodyScrollLock } from "@octavian-tocan/react-overlay";function MyOverlay({ isOpen }) {
useBodyScrollLock(isOpen);
return isOpen ?
Overlay content : null;
}
`Utilities
$3
Class name utility (clsx + tailwind-merge) for conditional classes.
`tsx
import { cn } from "@octavian-tocan/react-overlay";;
`TypeScript
Full TypeScript support with exported types:
`tsx
import type {
BottomSheetProps,
BottomSheetRef,
ModalProps,
ModalWrapperProps,
ModalSize,
ModalHeaderProps,
ModalDescriptionProps,
DismissButtonProps,
} from "@octavian-tocan/react-overlay";
`Custom Scrollbar Styling
The package includes optional CSS for custom scrollbar styling on scrollable content.
$3
Import the CSS file once in your app entry point:
`tsx
// In your app entry (e.g., main.tsx, App.tsx)
import "@octavian-tocan/react-overlay/styles/scrollbar.css";
`$3
Override CSS variables to match your theme:
`css
:root {
--ro-scrollbar-thumb: #6366f1; / Scrollbar color /
--ro-scrollbar-thumb-hover: #4f46e5; / Hover color /
--ro-scrollbar-width: 6px; / Width /
--ro-scrollbar-track: transparent; / Track color /
}
`$3
- BottomSheet: Scrollbar styling applied automatically
- ModalWrapper: Applied when
scrollable={true} (default)
- Custom elements: Add data-ro-scroll attribute`tsx
Scrollable content with custom styling
``MIT © Octavian Tocan