Modals. Drawers. Popovers. Bottom sheets. Confirmation dialogs. Dropdown panels.
They all look simple from the outside.
But once you start building them in a real React application, the logic around them becomes surprisingly complicated.
You need to think about:
- When the overlay should open
- How it should close
- Clicking outside
- Pressing
Escape - Preventing accidental dismissal
- Rendering above the application
- Managing focus
- Returning focus to the trigger
- Supporting nested overlays
- Keeping the implementation reusable
- Avoiding duplicated state and event-handling logic
- Making the UI work consistently across different screens
And if every feature implements its own version of this logic, your application eventually ends up with several slightly different modal and drawer implementations.
That is the problem I wanted to solve with useOverlay, one of the hooks in useThisHook.
A reusable React hook should remove repetitive engineering work without forcing your application into a particular UI design.
This article explains the problem, the design thinking behind useOverlay, practical usage patterns, and where a reusable overlay abstraction fits into a React application.
What exactly is an overlay?
An overlay is UI that appears above the normal application content.
Common examples include:
- Modal dialogs
- Confirmation dialogs
- Drawers
- Side panels
- Bottom sheets
- Popovers
- Contextual menus
- Full-screen overlays
- Command palettes
For example:
┌──────────────────────────────────────────────┐
│ Application │
│ │
│ ┌──────────────────────────────┐ │
│ │ │ │
│ │ Confirmation │ │
│ │ │ │
│ │ Cancel Continue │ │
│ │ │ │
│ └──────────────────────────────┘ │
│ │
└──────────────────────────────────────────────┘
The visual design is usually not the difficult part.
The difficult part is the behavior.
The hidden complexity behind a simple modal
A first implementation might look like this:
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>
Open
</button>
{isOpen && (
<div className="modal">
<h2>Delete account?</h2>
<button onClick={() => setIsOpen(false)}>
Cancel
</button>
</div>
)}
</>
);
Nothing wrong with this.
For a simple application, this might be completely sufficient.
The problem starts when you build several different overlays.
You may eventually have:
UserProfile
└── EditProfileModal
Orders
└── OrderDetailsDrawer
Checkout
└── PaymentConfirmationModal
Notifications
└── NotificationPopover
Settings
└── DeleteAccountModal
And suddenly each component has its own:
useState(...)
plus:
onClick(...)
plus:
onKeyDown(...)
plus:
useEffect(...)
plus:
createPortal(...)
plus:
document.addEventListener(...)
The UI is different.
But much of the behavior is the same.
That's where a hook becomes useful.
The idea behind useOverlay
The core idea is simple:
Separate overlay behavior from overlay presentation.
Your application should decide what the overlay looks like.
The hook should help manage the behavior required to control it.
Conceptually:
┌──────────────────┐
│ Application │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ useOverlay │
│ │
│ open │
│ close │
│ toggle │
│ state │
│ behavior │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Overlay UI │
│ │
│ Modal │
│ Drawer │
│ Popover │
│ Bottom Sheet │
└──────────────────┘
This distinction is important.
useOverlay should not dictate whether your application uses:
- Tailwind
- CSS Modules
- styled-components
- plain CSS
- a design system
- a component library
It should provide behavior that your UI can consume.
This is also a common design direction in existing React overlay systems. For example, React Aria's useOverlay focuses on overlay behavior such as outside interaction and Escape-key dismissal, while Primer's implementation also handles behaviors such as focus management. The APIs differ, but the underlying architectural idea is similar: separate behavior from presentation.
A simple useOverlay workflow
A typical application flow looks like this:
User clicks button
│
▼
open overlay
│
▼
overlay becomes visible
│
├───────────────┐
│ │
▼ ▼
User interacts User dismisses
│ │
│ ├── Escape
│ ├── Outside click
│ └── Close button
│
▼
Application continues
The important part is that the consuming component does not need to rebuild the same overlay-control mechanism every time.
Example: A confirmation dialog
Imagine an account deletion flow.
Without an overlay abstraction, you might write:
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const openDeleteDialog = () => {
setShowDeleteDialog(true);
};
const closeDeleteDialog = () => {
setShowDeleteDialog(false);
};
Then the component handles the dialog.
With useOverlay, the component can instead work with an overlay controller.
Conceptually:
const overlay = useOverlay();
const handleDelete = () => {
overlay.open();
};
And:
<button onClick={handleDelete}>
Delete account
</button>
The important improvement isn't that useOverlay magically makes a modal shorter.
The improvement is that overlay behavior can become a reusable primitive.
Overlay state should be predictable
One of the most important things when designing a reusable hook is predictable state.
An overlay generally has two primary states:
CLOSED
│
│ open()
▼
OPEN
│
│ close()
▼
CLOSED
And a toggle operation can be represented as:
CLOSED ── toggle() ──> OPEN
OPEN ── toggle() ──> CLOSED
This sounds trivial.
But centralizing that state transition makes the behavior consistent across components.
Instead of every feature inventing its own rules, the hook becomes the common interface.
open, close, and toggle
A useful overlay abstraction normally needs three basic operations.
open()
Open the overlay.
overlay.open();
Useful for:
- Opening a modal
- Opening a drawer
- Showing a panel
- Displaying a contextual UI
close()
Close the overlay.
overlay.close();
Useful for:
- Cancel buttons
- Save completion
- Escape-key handling
- Navigation events
toggle()
Switch between the two states.
overlay.toggle();
Useful for:
- Menus
- Expandable panels
- Popovers
- Toggle-based UI
The exact API exposed by a hook should remain small.
A reusable hook becomes harder to understand when it tries to become an entire UI framework.
The important distinction: behavior vs presentation
This is one of the reasons I prefer hooks for reusable UI infrastructure.
Consider a drawer.
One application might want:
┌───────────────────────┐
│ │
│ Drawer │
│ │
└───────────────────────┘
Another might want:
┌──────────────────────────────┐
│ ┌─────┤
│ │ │
│ │Drawer
│ │ │
│ └─────┤
└──────────────────────────────┘
Another might use a bottom sheet:
┌──────────────────────────────┐
│ │
│ │
│ │
├──────────────────────────────┤
│ Bottom Sheet │
└──────────────────────────────┘
The presentation is completely different.
The behavioral requirements are largely the same.
That's the abstraction boundary.
Where useOverlay becomes useful
There are several scenarios where a reusable overlay hook starts paying for itself.
1. Building a design system
If you're building an internal component library, you don't want every consuming application to reinvent overlay state management.
A shared hook can become the behavioral foundation for components such as:
Modal
Drawer
Popover
BottomSheet
ConfirmationDialog
ContextMenu
The components can then provide their own visual APIs.
2. Building a reusable component library
Suppose you are building:
<Modal />
<Drawer />
<Popover />
All three components need overlay behavior.
Instead of implementing it independently:
Modal
└── custom overlay logic
Drawer
└── custom overlay logic
Popover
└── custom overlay logic
you can structure the system as:
useOverlay
/ | \
/ | \
Modal Drawer Popover
Now the behavior has a single conceptual home.
3. Large enterprise applications
Enterprise applications often have dozens of screens.
Imagine a banking application containing:
- Customer details
- Account management
- Payments
- Loans
- Documents
- Notifications
- Support
- Settings
Each area may need multiple overlays.
Without shared primitives, overlay behavior tends to drift.
One modal closes when the user clicks outside.
Another doesn't.
One responds to Escape.
Another doesn't.
One restores focus.
Another doesn't.
These differences are easy to introduce accidentally.
A shared abstraction creates a consistent baseline.
Accessibility is not optional
This is where overlay implementations become significantly more serious.
A production overlay isn't simply:
position: fixed;
z-index: 9999;
There are behavioral concerns around:
- Keyboard navigation
- Focus management
- Escape-key handling
- Dismissal behavior
- Screen readers
- Focus restoration
- ARIA semantics
- Background interaction
- Nested overlays
Established React accessibility libraries explicitly treat these as part of overlay behavior. React Aria's overlay hook, for example, supports dismissal on outside interaction and Escape, while Primer's implementation includes focus-related behavior such as focus restoration.
This is why I would strongly recommend treating useOverlay as a behavioral primitive, not simply a convenient useState wrapper.
Don't confuse an overlay hook with a modal component
This distinction is worth emphasizing.
A component:
<Modal />
is responsible for presentation.
A hook:
useOverlay()
can provide behavior and state.
This gives you a composition model:
useOverlay
│
├── Modal
├── Drawer
├── BottomSheet
├── Popover
└── CustomOverlay
This makes the hook useful even when you don't use a predefined modal component.
Building a custom overlay
One of the strongest use cases for a generic hook is when your product has a custom interaction.
For example:
┌───────────────────────────────────────┐
│ │
│ Application │
│ │
│ ┌──────────────┐ │
│ │ │ │
│ │ Custom Panel │ │
│ │ │ │
│ └──────────────┘ │
│ │
└───────────────────────────────────────┘
You might not want to introduce an entire UI component library just to implement that panel.
You can keep your own markup and styling while reusing the overlay behavior.
That's where a headless approach becomes valuable.
Headless UI: why it matters
A headless primitive generally provides behavior without forcing a visual design.
For example:
const overlay = useOverlay();
Your application remains responsible for:
<div className="my-custom-overlay">
...
</div>
You decide:
- Colors
- Spacing
- Typography
- Borders
- Shadows
- Animations
- Responsive behavior
The hook should not care.
This makes the primitive much easier to integrate into existing design systems.
A common mistake: putting everything into the hook
There is another side to abstraction.
It's very easy to over-engineer a hook.
For example:
useOverlay
├── state
├── portal
├── animation
├── positioning
├── styling
├── focus
├── keyboard navigation
├── responsive behavior
├── routing
├── analytics
└── business logic
At that point, the hook isn't really a reusable primitive anymore.
It's becoming a framework.
That's not what I want useOverlay to become.
A good reusable hook should have a clear responsibility.
When should you NOT use useOverlay?
This is just as important.
If your application has one simple modal:
const [open, setOpen] = useState(false);
that's perfectly fine.
You don't need a library for everything.
I would consider introducing a reusable overlay abstraction when:
- Multiple components need overlay behavior
- You are building reusable UI components
- Your application has complex overlay interactions
- You want consistent behavior
- You are building a design system
- You want to centralize overlay-related logic
Abstraction has a cost.
Don't abstract code merely because you can.
Abstract when the repeated behavior has become a meaningful concept.
useOverlay vs useState
This isn't really:
useOverlay > useState
It's about different levels of abstraction.
useState
Use when you need simple local state:
const [isOpen, setIsOpen] = useState(false);
useOverlay
Use when the concept you're managing is specifically an overlay and you want reusable overlay behavior.
Think:
useState
↓
primitive state management
useOverlay
↓
overlay-specific behavior
The latter is valuable when overlay behavior becomes repeated infrastructure.
useOverlay vs a complete modal library
These solve different problems too.
A modal library may provide:
Modal
Dialog
Animation
Backdrop
Focus trap
ARIA
Styling
Theme
A hook can instead provide a smaller behavioral primitive.
That makes it attractive when:
- You already have a design system
- You already have your own modal component
- You don't want another styling system
- You need a custom UI
- You want a smaller abstraction
There are also established libraries with sophisticated overlay implementations. React Aria, for example, provides overlay behavior specifically around accessibility and dismissal, while other projects combine positioning and animation into a broader floating-element abstraction.
So the goal of useThisHook isn't to claim that every application needs another overlay library.
The goal is to provide small, composable React primitives that solve common problems without forcing an entire UI architecture.
A useful mental model
When deciding whether to create or use a hook, ask:
"What repeated behavior exists independently of the UI?"
For overlays:
UI
│
├── Modal
├── Drawer
├── Bottom Sheet
├── Popover
└── Custom Panel
But underneath:
Behavior
│
├── Open
├── Close
├── Toggle
├── Dismiss
├── Focus
└── Interaction
That's the layer where a hook can provide value.
Testing overlay behavior
Reusable infrastructure deserves tests.
At minimum, test the important state transitions.
Opening
closed → open
Closing
open → closed
Toggling
closed → open
open → closed
Repeated operations
open()
open()
close()
close()
The hook should behave predictably.
If the overlay also manages interaction behavior, test:
- Escape
- Outside interaction
- Focus behavior
- Cleanup
- Multiple overlays
- Unmounting
The exact test suite depends on the responsibilities of the implementation.
Why I built useThisHook
useThisHook started from a simple observation:
React applications repeatedly solve the same small problems.
Sometimes the problem is:
"How do I debounce this?"
Sometimes:
"How do I detect an outside click?"
Sometimes:
"How do I manage an overlay?"
Sometimes:
"How do I keep this input behavior consistent?"
None of these problems necessarily deserves an entire application-level abstraction.
But they can be excellent candidates for small, reusable hooks.
That's the philosophy behind useThisHook.
The goal isn't to hide React.
It's to remove repetitive implementation work while keeping the underlying behavior understandable.
Explore useOverlay
You can try the interactive useOverlay documentation and examples here:
https://usethishook.mentorbridge.in/useOverlay
The important part of a hook library isn't the README.
It's being able to understand what the hook does, interact with it, and quickly determine whether it solves your problem.
That's why useThisHook focuses on interactive examples alongside the API documentation.
What I'd look for before adding an overlay abstraction
Before adding any reusable hook to a project, I ask five questions:
1. Is the problem repeated?
If it happens once, don't abstract it.
2. Does the behavior have a clear boundary?
If you can't describe what the hook owns in one sentence, the abstraction may be too broad.
3. Can the hook remain UI-agnostic?
The more presentation the hook owns, the less reusable it becomes.
4. Can developers understand it quickly?
A reusable abstraction should reduce cognitive load, not increase it.
5. Can it be tested independently?
If the behavior can't be isolated and tested, the abstraction probably needs another look.
Final thoughts
Overlay UI looks like a visual problem.
In a production React application, it is primarily a behavioral problem.
The modal itself is usually easy.
The difficult part is everything around it.
That's why a small abstraction such as useOverlay can be useful when an application starts building multiple overlay-based interactions.
The key is keeping the abstraction focused:
Application
│
▼
useOverlay
│
├── Modal
├── Drawer
├── Popover
├── Bottom Sheet
└── Custom Overlay
The application owns the design.
The hook owns reusable behavior.
And React remains the composition layer connecting the two.
If you're building reusable React components, design systems, or simply tired of rewriting overlay state and interaction logic across different features, take a look at useOverlay:
👉 https://usethishook.mentorbridge.in/useOverlay
And if you find an edge case, a missing capability, or a better way to design the API, that's exactly the kind of feedback that makes an open-source hook library better.
About useThisHook
useThisHook is an open-source collection of reusable React hooks designed around practical problems developers encounter while building real applications.
The goal is simple:
Stop rewriting the same React hooks.
Explore the collection:
https://usethishook.mentorbridge.in/
Source code and contributions:
https://github.com/senthilkumar979/useThisHook
Tags
React ReactJS JavaScript Web Development Frontend Development Hooks Open Source UI Architecture Design Systems

Top comments (0)