Documentation

Introduction

A headless popup system designed for building accessible, composable overlays such as modals, drawers and sidebars.

It provides a unified API for managing multiple overlays while maintaining full control over styling and layout.

Why This Exists

Most modal implementations rely on isolated boolean state and tightly coupled UI, which limits composability and makes it difficult to support more advanced overlay patterns.

As application grow, these limitations lead to fragmented logic, inconsistent behavior, and poor accessibility support.

This system introduce a headless, stack-driven architecture that centralizes overlay state and enables scalable composition of modals, drawers, and sidebars through a unified API.

Architecture

The system is structured around two core layers: a global stack manager and isolated popup instances.

A top-level provider maintains a centralized stack that tracks active popups, enabling proper layering and control of multiple overlays.

Each popups instance establishes its own context boundary, allowing triggers and content components to communicate without relying on prop drilling or shared global state.

PopupProvider (global stack)
   ↓
Popup (instance boundary)
   ↓         
Trigger / Content / Close

This separation ensures predictable behavior, scalability, and composability across different overlay types.

Design Decision

  • Stack-based state over boolean flags: Enables multiple popups to coexist and ensures only the topmost overlay is interactive at any time
  • Compound component pattern: Provides a declarative API where related components share state implicitly through context.
  • Headless architecture: Separates behavior from presentation, allowing full control over styling and layout without constraints.
  • Context isolation per instance: Ensures each popup operates independently, preventing unintended interactions between multiple instances.
  • Portal-based rendering: Avoids stacking and layout conflicts by rendering overlays outside the normal DOM hierarchy.

Stack System

Popups are managed using a Last-In-First-Out (LIFO) stack model.

Code example
type PopupStackItem = {
  id: string;
};
  • Opening a popup pushes it onto the stack.
  • Closing removes it from the stack.
  • The topmost popup is always the active one.

Variants

Variants control layout and animation behavior while sharing the same underlying logic.

'modal' | 'drawer-left' | 'drawer-right'

Each variant applies different positioning and transitions using state-driven styles.

Overlay Behavior

The system includes built-in overlay handling to ensure a consistent and accessible user experience across all popup variants.

Scroll Lock

When a popup is open, background scrolling is disabled to prevent interaction with underlying content.

Overlay Click to Close

Clicking outside the popup content (on the overlay) will close the active popup.

This behavior is scoped to the topmost popup in the stack, preventing unintended closures when multiple overlays are present.

Code example
useOverlay(containerRef, {
  isOpen,
  onClose,
});

The overlay logic is encapsulated in a reusable hook, which handles outside clicks and escape key interactions while respecting the popup stack.

Escape Key Handling

Pressing the escape key closes the topmost popup, maintaining predictable and accessible interaction patterns.

API

Code example
<Popup>
  <Popup.Trigger asChild>
    <button type='button'>Open</button>
  </Popup.Trigger>

  <Popup.Content variant='modal'>
    <Popup.Header>
      <Popup.Title>Title</Popup.Title>
      <Popup.Close>X</Popup.Close>
    </Popup.Header>

    <Popup.Body>Content</Popup.Body>
  </Popup.Content>
</Popup>

Keyboard Interactions

The system provides built-in keyboard support to ensure accessibility and predictable interaction across all popup variants.

Focus Trap

When a popup is active, focus is contained within the overlay, preventing interaction with elements outside of it.

This ensures users navigating via keyboard can cycle through interactive elements without leaving the popup context.

Code example
useFocusTrap(containerRef, isOpen);
Code example
const focusable = container.querySelectorAll(
  'a, button, input, textarea, select, [tabindex]:not([tabindex='-1'])'
);

The focus trap is implemented using a custom hook that manages keyboard navigation and ensures focus loops between the first and last focusable elements.

Escape Key

Pressing the Escape key closes the topmost popup in the stack, ensuring consistent and expected behavior.

Tab Navigation

  • Esc - closes the topmost popup
  • Tab - moves focus forward
  • Shift + Tab - moves focus backward

Live Examples

Modal Example

Drawer Left Example

Drawer Right Example

Stacked Popup Example