@octavian-tocan/react-overlay
v1.7.1
Published
Unified overlay components for React: BottomSheet with snap points and Modal with animations
Maintainers
Readme
@octavian-tocan/react-overlay
Unified overlay components for React: BottomSheet with snap points and Modal with animations.
Installation
npm install @octavian-tocan/react-overlay
# or
pnpm add @octavian-tocan/react-overlay
# or
yarn add @octavian-tocan/react-overlayPeer Dependencies
npm install react react-dom motion clsx tailwind-merge
# Optional: lucide-react (for DismissButton icon)
npm install lucide-reactQuick Start (Copy & Paste)
BottomSheet
import { 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>
</>
);
}Modal
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>
</>
);
}Documentation
Components
BottomSheet
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>
);
}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:
// 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 |
Gradient Background Example
<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>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:
<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>Modal
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>
);
}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 |
ModalWrapper
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>;Helper Components
ModalHeader
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" />;ModalDescription
Styled description text.
import { ModalDescription } from "@octavian-tocan/react-overlay";
<ModalDescription>This action cannot be undone.</ModalDescription>;DismissButton
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 |
Hooks
useBodyScrollLock
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;
}Utilities
cn
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)} />;TypeScript
Full TypeScript support with exported types:
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.
Setup
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";Customization
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 */
}Usage
- 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>License
MIT © Octavian Tocan
