Unified overlay components for React: BottomSheet with snap points and Modal with animations.
npm install @octavian-tocan/react-overlay
# or
pnpm add @octavian-tocan/react-overlay
# or
yarn add @octavian-tocan/react-overlaynpm install react react-dom motion clsx tailwind-merge
# Optional: lucide-react (for DismissButton icon)
npm install lucide-reactimport { useState } from "react";
import { BottomSheet } from "@octavian-tocan/react-overlay";
export function MyBottomSheet() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Open Sheet</button>
<BottomSheet
open={open}
onDismiss={() => setOpen(false)}
snapPoints={({ maxHeight }) => [200, maxHeight * 0.5, maxHeight * 0.9]}
>
<div className="p-4">
<h2>Sheet Content</h2>
<p>Drag to resize or swipe down to dismiss.</p>
</div>
</BottomSheet>
</>
);
}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 (
<>
<button onClick={() => setOpen(true)}>Open Modal</button>
<Modal open={open} onDismiss={() => setOpen(false)} size="md">
<ModalHeader icon={<AlertCircle className="w-4 h-4 text-white" />} title="Confirm Action" />
<ModalDescription>Are you sure you want to proceed?</ModalDescription>
<div className="flex gap-2 mt-4 justify-end">
<button onClick={() => setOpen(false)}>Cancel</button>
<button
onClick={() => {
/* handle confirm */ setOpen(false);
}}
>
Confirm
</button>
</div>
</Modal>
</>
);
}A draggable bottom sheet with snap points, spring animations, and swipe-to-dismiss.
import { BottomSheet } from "@octavian-tocan/react-overlay";
function App() {
const [open, setOpen] = useState(false);
return (
<BottomSheet
open={open}
onDismiss={() => setOpen(false)}
snapPoints={({ maxHeight }) => [200, maxHeight * 0.5, maxHeight * 0.9]}
>
<p>Sheet content here</p>
</BottomSheet>
);
}| 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 |
| 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) |
The unstyled prop removes default backgrounds and padding for full customization:
// Remove all default styling
<BottomSheet unstyled>...</BottomSheet>
// Selectively remove styling
<BottomSheet unstyled={{ sheet: true, content: true, handle: false }}>...</BottomSheet>| Option | Effect |
|---|---|
sheet |
Removes white background from sheet |
content |
Removes padding from content area |
handle |
Hides the drag handle pill |
<BottomSheet
open={open}
onDismiss={() => setOpen(false)}
unstyled={{ sheet: true, content: true }}
sheetClassName="bg-gradient-to-b from-sky-400 to-sky-600"
contentClassName="p-6"
>
<div className="text-white">
<h2>Sign in to continue</h2>
<p>Create an account to get started</p>
</div>
</BottomSheet>For dark-themed sheets, use handleStyle to customize the handle pill color and the unstyled dismiss button variant:
<BottomSheet
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" },
}}
>
<div className="text-white p-6">
<h2>Dark Theme Sheet</h2>
<p>Custom handle and dismiss button styling</p>
</div>
</BottomSheet>A centered modal dialog with size presets and Motion animations.
import { Modal, ModalHeader, ModalDescription } from "@octavian-tocan/react-overlay";
import { AlertCircle } from "lucide-react";
function App() {
const [open, setOpen] = useState(false);
return (
<Modal open={open} onDismiss={() => setOpen(false)} size="md">
<ModalHeader icon={<AlertCircle className="w-4 h-4 text-white" />} title="Confirm Action" />
<ModalDescription>Are you sure you want to proceed?</ModalDescription>
{/* Your content */}
</Modal>
);
}| 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 |
Low-level modal component for full customization.
import { ModalWrapper } from "@octavian-tocan/react-overlay";
<ModalWrapper
open={isOpen}
onDismiss={handleClose}
contentClassName="bg-white rounded-xl p-8 max-w-lg"
showDismissButton
>
{children}
</ModalWrapper>;Header with icon badge and title.
import { ModalHeader } from "@octavian-tocan/react-overlay";
import { AlertCircle } from "lucide-react";
<ModalHeader icon={<AlertCircle className="w-4 h-4 text-white" />} title="Confirm Delete" />;Styled description text.
import { ModalDescription } from "@octavian-tocan/react-overlay";
<ModalDescription>This action cannot be undone.</ModalDescription>;Close button for overlays.
import { DismissButton } from "@octavian-tocan/react-overlay";
<DismissButton
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 |
Lock body scroll when an overlay is open. Supports multiple concurrent overlays via ref-counting.
import { useBodyScrollLock } from "@octavian-tocan/react-overlay";
function MyOverlay({ isOpen }) {
useBodyScrollLock(isOpen);
return isOpen ? <div>Overlay content</div> : null;
}Class name utility (clsx + tailwind-merge) for conditional classes.
import { cn } from "@octavian-tocan/react-overlay";
<div className={cn("base-class", isActive && "active-class", className)} />;Full TypeScript support with exported types:
import type {
BottomSheetProps,
BottomSheetRef,
ModalProps,
ModalWrapperProps,
ModalSize,
ModalHeaderProps,
ModalDescriptionProps,
DismissButtonProps,
} from "@octavian-tocan/react-overlay";The package includes optional CSS for custom scrollbar styling on scrollable content.
Import the CSS file once in your app entry point:
// In your app entry (e.g., main.tsx, App.tsx)
import "@octavian-tocan/react-overlay/styles/scrollbar.css";Override CSS variables to match your theme:
: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 */
}- BottomSheet: Scrollbar styling applied automatically
- ModalWrapper: Applied when
scrollable={true}(default) - Custom elements: Add
data-ro-scrollattribute
<div data-ro-scroll className="overflow-auto max-h-96">
Scrollable content with custom styling
</div>MIT © Octavian Tocan
