Vitus Labs
Elements

Overlay

Advanced positioning system for dropdowns, tooltips, popovers, and modals.

Overlay is a complete positioning and interaction system for floating UI elements. It handles trigger/content refs, dynamic positioning with viewport edge flipping, event listeners, and nested overlay blocking.

Basic Usage

import { Overlay } from '@vitus-labs/elements'

// Dropdown
<Overlay trigger={<button>Menu</button>}>
  <div>Dropdown content</div>
</Overlay>

// Tooltip on hover
<Overlay
  type="tooltip"
  openOn="hover"
  closeOn="hover"
  align="top"
  trigger={<span>Hover me</span>}
>
  <div>Tooltip text</div>
</Overlay>

Props

Content

PropTypeDescription
triggerReactNode | (props) => ReactNodeTrigger element or render function
childrenReactNode | (props) => ReactNodeContent to show when open
DOMLocationHTMLElementPortal target (default: document.body)
triggerRefNamestringRef prop name for trigger (default: 'ref')
contentRefNamestringRef prop name for content (default: 'ref')

Behavior

PropTypeDefaultDescription
isOpenbooleanfalseInitial open state
openOn'click' | 'hover' | 'manual''click'How to open
closeOn'click' | 'clickOnTrigger' | 'clickOutsideContent' | 'hover' | 'manual''click'How to close
closeOnEscbooleantrueClose on ESC key
disabledbooleanDisable completely
onOpen() => voidCalled on open
onClose() => voidCalled on close

Positioning

PropTypeDefaultDescription
type'dropdown' | 'tooltip' | 'popover' | 'modal' | 'custom''dropdown'Positioning preset
position'absolute' | 'fixed' | 'relative' | 'static''fixed'CSS position
align'top' | 'bottom' | 'left' | 'right''bottom'Primary direction
alignX'left' | 'center' | 'right''left'Horizontal alignment
alignY'top' | 'center' | 'bottom''bottom'Vertical alignment
offsetXnumber0Horizontal margin
offsetYnumber0Vertical margin
throttleDelaynumber200Position recalculation throttle (ms)
parentContainerHTMLElementCustom scroll container

Render Props

When using render functions, trigger and content receive interaction props:

Trigger Render Props

<Overlay
  trigger={(props) => (
    <button ref={props.ref}>
      {props.active ? 'Close' : 'Open'}
    </button>
  )}
>
  <div>Content</div>
</Overlay>
PropTypeDescription
activebooleanWhether content is visible
showContent() => voidOpen the overlay
hideContent() => voidClose the overlay

Content Render Props

<Overlay trigger={<button>Open</button>}>
  {(props) => (
    <div ref={props.ref}>
      Aligned: {props.align} / {props.alignX}
      <button onClick={props.hideContent}>Close</button>
    </div>
  )}
</Overlay>
PropTypeDescription
activebooleanWhether content is visible
alignstringCurrent primary direction (may flip)
alignXstringCurrent horizontal alignment
alignYstringCurrent vertical alignment
showContent() => voidOpen
hideContent() => voidClose

Positioning Types

Positioned relative to trigger, below by default. Flips to top if not enough space.

<Overlay type="dropdown" align="bottom" alignX="left">
  ...
</Overlay>

Tooltip

Similar to dropdown but typically used with hover:

<Overlay type="tooltip" openOn="hover" closeOn="hover" align="top">
  ...
</Overlay>

Centered in viewport:

<Overlay
  type="modal"
  openOn="click"
  closeOn="clickOutsideContent"
  closeOnEsc
>
  {(props) => (
    <div style={{ background: 'white', padding: 24, borderRadius: 8 }}>
      Modal content
      <button onClick={props.hideContent}>Close</button>
    </div>
  )}
</Overlay>

Dynamic Positioning

Overlay automatically flips direction when content would go off-screen:

  • If align="bottom" but there's no space below, flips to "top"
  • If alignX="left" but content overflows right, adjusts to "right"
  • Center alignment centers the content relative to the trigger

How Positioning Works

  1. Content mounts in portalcontentRef callback fires, signals ready
  2. getBoundingClientRect() — Measures trigger and content dimensions
  3. Viewport check — Tests if content fits in preferred direction (fitsTop, fitsBottom, fitsLeft, fitsRight)
  4. Auto-flip — If preferred direction doesn't fit, selects the best alternative
  5. Offset application — Adds offsetX/offsetY margins
  6. Style assignment — Sets position, top, left, right, bottom directly on the content element

Absolute vs Fixed Positioning

  • position: 'fixed' (default): Content positioned relative to the viewport
  • position: 'absolute': Content positioned relative to its offsetParent. The system automatically subtracts the offsetParent's viewport rect to compute correct placement.

When type="modal" and the overlay is active:

  • document.body.style.overflow = 'hidden' — prevents background scrolling
  • Reset to original on close

Performance

  • Position recalculation is throttled by throttleDelay (default: 200ms)
  • Scroll and resize listeners use { passive: true } for better scrolling performance
  • useLayoutEffect + requestAnimationFrame — positions immediately, then re-measures after browser reflow
  • Throttled callbacks use the "latest ref" pattern to access current state without stale closures

Nested Overlays

Child overlays automatically block parent from closing when interacting with nested content:

<Overlay trigger={<button>Main Menu</button>}>
  {(props) => (
    <div ref={props.ref}>
      <div>Item 1</div>
      <Overlay trigger={<div>Submenu</div>} align="right">
        {(subProps) => (
          <div ref={subProps.ref}>
            <div>Sub Item 1</div>
            <div>Sub Item 2</div>
          </div>
        )}
      </Overlay>
    </div>
  )}
</Overlay>

useOverlay Hook

For full control without the Overlay component:

import { useOverlay } from '@vitus-labs/elements'

function CustomDropdown() {
  const {
    active,
    triggerRef,
    contentRef,  // callback ref!
    showContent,
    hideContent,
    Provider,
    ...ctx
  } = useOverlay({
    openOn: 'click',
    closeOnEsc: true,
  })

  return (
    <>
      <button ref={triggerRef} onClick={showContent}>
        Toggle
      </button>
      {active && (
        <Portal>
          <Provider {...ctx}>
            <div ref={contentRef}>Content</div>
          </Provider>
        </Portal>
      )}
    </>
  )
}

Important: contentRef is a callback ref — assign it directly as ref={contentRef}.

useOverlay Return Value

PropertyTypeDescription
triggerRefRefObject<HTMLElement>Object ref for trigger element
contentRef(node: HTMLElement) => voidCallback ref for content element
activebooleanCurrent open/close state
alignstringResolved primary direction (after flipping)
alignXstringResolved horizontal alignment (after flipping)
alignYstringResolved vertical alignment (after flipping)
showContent() => voidOpen the overlay
hideContent() => voidClose the overlay
blockedbooleanWhether a nested overlay is blocking close
setBlocked() => voidSignal that a child overlay is active
setUnblocked() => voidSignal that a child overlay closed
ProviderFCContext provider for nested overlay coordination

Event Listeners

When the overlay is active, these listeners are attached (web only):

EventTargetConditionPurpose
keydown (ESC)windowcloseOnEsc && activeClose on Escape
resizewindowactiveRecalculate position
scrollwindow (passive)activeRecalculate position
clickwindowopenOn='click' or closeOn includes clickOpen/close handling
mouseenter/mouseleavetrigger + contentopenOn='hover' or closeOn='hover'Hover interaction with 100ms bridge

All listeners are cleaned up on unmount or when the overlay closes.

API Reference

Prop

Type

On this page