overlay-manager-rc
v1.0.4
Published
Lightweight (2KB) React overlay manager with zero dependencies, hook-based API
Downloads
200
Maintainers
Readme
Overlay-manager-rc
English | 한국어
🚀 Live Demo
Inspired by angular cdk overlay
Lightweight (2KB), zero-dependency React overlay manager with hook-based API
📢 Upgrading from v0.9.x? See the Migration Guide
Table of Contents
Overview
In React applications, many overlay component codes such as dialogs, alerts, and sheets can cause maintenance difficulties:
- ❌ Manually managing open/close state in parent components
- ❌ Props drilling through multiple components
- ❌ Complex state management setup (Redux, Zustand, etc.)
- ❌ SSR hydration issues with IDs
- ❌ Memory leaks from forgotten cleanup
overlay-manager-rc solves all of this with:
- 📦 Zero Dependencies - No external dependencies, only peer deps on React
- 🪶 Lightweight - ~2KB minified + gzipped, smaller than a single image
- 🎯 Hook-based API - Clean and intuitive API with
useOverlay()hook - 🔄 No state management - Open/close state handled automatically
- 🆔 SSR-safe - Works seamlessly with Next.js, Remix, and other SSR frameworks
- 🎁 Type-safe - Full TypeScript support with generics
- 🔁 Promise-based - Natural async/await API for overlay results
- 🎭 Lifecycle callbacks -
onOpen,onClose,beforeClosefor fine-grained control - 🔒 Smart ID management - Auto-closes existing overlay when opening with same ID
- ⚡ Automatic cleanup - Closed overlays removed after animations
- ⚛️ React 18+ & 19 - Compatible with latest React versions
Perfect For
- Radix UI / shadcn/ui users - Works seamlessly with headless UI libraries
- Next.js projects - SSR-safe with no hydration issues
- TypeScript projects - Full type inference for overlay data
- Performance-conscious apps - Minimal bundle impact (~2KB)
- Complex overlay flows - Sequential dialogs, confirmation chains, multi-step forms
What Makes It Different?
The Problem: Managing overlays typically requires managing state in parent components, passing props, and writing lots of boilerplate code.
The Solution: Function-based overlay management - no state, no props, just simple function calls.
Traditional Way (Without overlay-manager-rc)
// ❌ Parent component manages state
function ParentComponent() {
const [isOpen, setIsOpen] = useState(false);
const [dialogData, setDialogData] = useState(null);
const handleOpen = () => {
setDialogData({ userId: 123 });
setIsOpen(true);
};
const handleClose = (result) => {
setIsOpen(false);
// Handle result...
};
return (
<>
<Button onClick={handleOpen}>Open</Button>
<MyDialog
isOpen={isOpen}
onClose={handleClose}
data={dialogData}
/>
</>
);
}
// Dialog component needs props drilling
function MyDialog({ isOpen, onClose, data }) {
return (
<Dialog open={isOpen} onOpenChange={onClose}>
{/* Use data here */}
</Dialog>
);
}With overlay-manager-rc
// ✅ Parent component stays clean
function ParentComponent() {
const { openOverlay } = useOverlayManager();
const handleOpen = async () => {
const result = await openOverlay({
content: MyDialog,
data: { userId: 123 }
});
// Handle result directly!
};
return <Button onClick={handleOpen}>Open</Button>;
}
// Dialog component accesses data via hook
function MyDialog() {
const { isOpen, overlayData, closeOverlay } = useOverlay();
return (
<Dialog open={isOpen} onOpenChange={() => closeOverlay()}>
{/* Use overlayData directly */}
</Dialog>
);
}Key Benefits
1. No State in Parent Components
// ❌ Before: Manual state management
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [isAlertOpen, setIsAlertOpen] = useState(false);
const [isSheetOpen, setIsSheetOpen] = useState(false);
// ✅ After: Just open when needed
openOverlay({ content: Dialog });
openOverlay({ content: Alert });
openOverlay({ content: Sheet });2. Promise-based Results
// ✅ Get results directly
const result = await openOverlay({
content: ConfirmDialog,
data: { message: 'Delete this?' }
});
if (result === 'confirmed') {
await deleteItem();
}3. Sequential Flows Made Easy
// ✅ Chain overlays naturally
async function checkoutFlow() {
const address = await openOverlay({ content: AddressForm });
const payment = await openOverlay({ content: PaymentForm, data: address });
const confirmed = await openOverlay({ content: ConfirmOrder, data: payment });
if (confirmed) {
await processOrder();
}
}4. Type-Safe Data Passing
// ✅ Full type inference
interface FormData { name: string; email: string; }
const result = await openOverlay<FormData, boolean>({
content: MyForm,
data: { name: '', email: '' }
});
// result is typed as boolean | undefined5. No Props Drilling
// ❌ Before: Props through multiple levels
<Dialog>
<DialogContent userId={userId}>
<UserProfile userId={userId}>
<UserActions userId={userId} />
</UserProfile>
</DialogContent>
</Dialog>
// ✅ After: Access data anywhere
function UserActions() {
const { overlayData } = useOverlay<{ userId: number }>();
// Use overlayData.userId directly
}6. Automatic Cleanup
// ❌ Before: Manual cleanup needed
useEffect(() => {
return () => {
// Remember to clean up!
};
}, []);
// ✅ After: Automatic cleanup
// Just close the overlay - cleanup happens automatically
closeOverlay();7. Function-Based Management = Better Reusability
// ❌ Before: JSX declaration - hard to reuse
function UserList() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Delete</Button>
<ConfirmDialog
isOpen={isOpen}
onClose={() => setIsOpen(false)}
message="Delete this user?"
/>
</>
);
}
// Need to copy-paste this dialog in every component! 😱
// ✅ After: Reusable function - call anywhere
// utils/overlays.ts
export async function confirmDelete(itemName: string) {
return await openOverlay({
content: ConfirmDialog,
data: {
title: 'Confirm Delete',
message: `Delete ${itemName}?`
}
});
}
// Use in any component!
function UserList() {
const handleDelete = async (user) => {
const confirmed = await confirmDelete(user.name);
if (confirmed) await deleteUser(user.id);
};
}
function ProductList() {
const handleDelete = async (product) => {
const confirmed = await confirmDelete(product.name);
if (confirmed) await deleteProduct(product.id);
};
}8. Easy Refactoring
// ✅ Business logic separated from UI
// services/user-service.ts
export async function deleteUserWithConfirm(userId: number) {
const user = await fetchUser(userId);
// Step 1: Confirm
const confirmed = await openOverlay({
content: ConfirmDialog,
data: { message: `Delete ${user.name}?` }
});
if (!confirmed) return false;
// Step 2: Show loading
const loadingOverlay = openOverlay({
content: LoadingDialog,
data: { message: 'Deleting...' }
});
// Step 3: Delete
await api.delete(`/users/${userId}`);
closeOverlay(loadingOverlay);
// Step 4: Success message
await openOverlay({
content: SuccessDialog,
data: { message: 'User deleted!' }
});
return true;
}
// Component stays clean!
function UserActions({ userId }) {
return (
<Button onClick={() => deleteUserWithConfirm(userId)}>
Delete
</Button>
);
}Installation
npm
npm install overlay-manager-rcyarn
yarn add overlay-manager-rcpnpm
pnpm add overlay-manager-rcQuick Start
Step 1: Add OverlayContainer
Example with Next.js (App Router) + shadcn/ui (Radix UI)
Create overlay-container-provider.tsx:
'use client';
import type { ReactNode } from 'react';
import { OverlayContainer } from "overlay-manager-rc";
export function OverlayContainerNext({ children }: { children?: ReactNode }) {
return <OverlayContainer/>;
}Step 2: Add to Layout
Add the container to your layout.tsx:
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body className={cn('min-h-screen font-sans antialiased dark')}>
{children}
<OverlayContainerNext />
</body>
</html>
);
}Usage
Create Overlay Component
Access overlay context using the useOverlay() hook:
import { useOverlay } from 'overlay-manager-rc';
export function DemoAlertDialog() {
// Access overlay context via hook
const { overlayId, isOpen, overlayData, closeOverlay, dismiss } = useOverlay<string>();
return (
<AlertDialog
onOpenChange={(v) => {
!v && dismiss(); // Or use closeOverlay() - both work the same
}}
open={isOpen}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Alert title</AlertDialogTitle>
<AlertDialogDescription>
Get Data: {overlayData}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={dismiss}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={() => closeOverlay('confirmed')}>
Continue
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}Open Overlay
'use client';
import { useOverlayManager } from 'overlay-manager-rc';
export function AlertSection() {
const { openOverlay } = useOverlayManager();
const handleOpenAlert = async () => {
const result = await openOverlay({
content: DemoAlertDialog,
data: 'hello!!!!',
onClose: (result) => {
console.log('Dialog closed with result:', result);
},
onOpen: (id) => {
console.log('Overlay opened with id:', id);
},
});
console.log('Result from openOverlay:', result); // Same value as onClose result
};
return (
<section className="md:h-screen">
<div className="flex flex-col gap-10">
<Button onClick={handleOpenAlert}>
show alert
</Button>
</div>
</section>
);
}Manual ID Management
When you specify a manual ID and an overlay with the same ID is already open, the existing overlay will automatically close before opening the new one.
'use client';
import { useOverlayManager } from 'overlay-manager-rc';
export function AlertSection() {
const { openOverlay } = useOverlayManager();
const handleOpenAlert = async () => {
// This will close any existing overlay with ID 'custom-alert'
// before opening the new one
await openOverlay({
id: 'custom-alert',
content: DemoAlertDialog,
data: 'first alert!',
});
};
const handleOpenAnotherAlert = async () => {
// If 'custom-alert' is already open, it will close first
await openOverlay({
id: 'custom-alert',
content: DemoAlertDialog,
data: 'second alert!',
});
};
return (
<section className="md:h-screen">
<div className="flex flex-col gap-10">
<Button onClick={handleOpenAlert}>First Alert</Button>
<Button onClick={handleOpenAnotherAlert}>Second Alert</Button>
</div>
</section>
);
}API Reference
useOverlayManager
Returns an object with overlay management functions.
| Name | Description | Parameter | | --- | --- | --- | | openOverlay | Opens an overlay component. Returns a Promise that resolves with the close result. | OverlayOptions | | closeOverlay | Closes an overlay component by ID. | id: string | | closeAllOverlays | Closes all overlay components. | - | | overlays | Array of all current overlay states. | - |
OverlayOptions<TData, TResult>
| Prop | Type | Default | Required | | --- | --- | --- | --- | | id | string | Auto-generated | No | | content | ComponentType (React Component) | - | Yes | | data | TData | - | No | | onClose | (result?: TResult) => void | Promise | - | No | | onOpen | (id: string) => void | Promise | - | No | | beforeClose | () => boolean | Promise | - | No |
useOverlay()
Hook for accessing overlay context inside overlay components. Must be used within an overlay component rendered by OverlayContainer.
Returns:
| Property | Type | Description |
| --- | --- | --- |
| overlayId | string | Unique ID of the overlay |
| isOpen | boolean | Whether the overlay is currently open |
| overlayData | TData | Data passed to the overlay via openOverlay() |
| closeOverlay | (result?: TResult) => void | Function to close the overlay with optional result |
| dismiss | () => void | Function to dismiss (cancel) the overlay without returning a result. Same as closeOverlay() |
useBeforeClose
Hook that executes logic before closing the overlay. Used to prevent closing based on conditions (e.g., unsaved changes).
Usage:
import { useOverlay, useBeforeClose } from 'overlay-manager-rc';
export function FormOverlay() {
const { overlayId, overlayData, closeOverlay } = useOverlay();
const [isDirty, setIsDirty] = useState(false);
useBeforeClose(async () => {
if (isDirty) {
const canClose = window.confirm('You have unsaved changes. Are you sure?');
return canClose; // true = allow close, false = prevent close
}
return true;
}, overlayId);
// ... rest of component
}Browser Support
- Modern browsers with ES2020+ support
- Server-side rendering frameworks (Next.js, Remix, Gatsby, etc.)
- React 18.0.0+ or React 19.0.0+
License
MIT
