← Back to all hooks

usePortal

Create and manage portals for rendering content outside the component tree. Perfect for modals, tooltips, and overlays.

Live Demo

🔍 What's a Portal?

Portals render content outside the component tree, typically at the end of document.body. This is useful for modals, tooltips, and overlays that need to escape parent overflow/z-index constraints.

📝 Portal Events

No portal events yet. Try the buttons above!

Code Examples

Basic Modal

import { usePortal } from "@uiblock/hooks";

function Modal({ isOpen, onClose, children }) {
  const { Portal } = usePortal({ enabled: isOpen });

  if (!isOpen) return null;

  return (
    <Portal>
      <div className="modal-overlay" onClick={onClose}>
        <div className="modal-content" onClick={e => e.stopPropagation()}>
          {children}
          <button onClick={onClose}>Close</button>
        </div>
      </div>
    </Portal>
  );
}

With Custom Container

import { usePortal } from "@uiblock/hooks";

function Notification({ message }) {
  const { Portal } = usePortal({
    id: 'notification-portal', // Reuse existing element
    enabled: true
  });

  return (
    <Portal>
      <div className="notification">
        {message}
      </div>
    </Portal>
  );
}

Tooltip with Portal

import { usePortal } from "@uiblock/hooks";
import { useState } from "react";

function TooltipButton() {
  const [showTooltip, setShowTooltip] = useState(false);
  const { Portal } = usePortal({ enabled: showTooltip });

  return (
    <>
      <button
        onMouseEnter={() => setShowTooltip(true)}
        onMouseLeave={() => setShowTooltip(false)}
      >
        Hover me
      </button>
      {showTooltip && (
        <Portal>
          <div className="tooltip">
            This is a tooltip!
          </div>
        </Portal>
      )}
    </>
  );
}

Multiple Portals

import { usePortal } from "@uiblock/hooks";

function App() {
  const { Portal: ModalPortal } = usePortal({ id: 'modal-root' });
  const { Portal: TooltipPortal } = usePortal({ id: 'tooltip-root' });
  const { Portal: NotificationPortal } = usePortal({ id: 'notification-root' });

  return (
    <>
      <ModalPortal>
        <div>Modal content</div>
      </ModalPortal>
      <TooltipPortal>
        <div>Tooltip content</div>
      </TooltipPortal>
      <NotificationPortal>
        <div>Notification content</div>
      </NotificationPortal>
    </>
  );
}

How It Works

Here's the implementation:

import { useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'

export function usePortal(options = {}) {
  const { id, parent, enabled = true } = options
  const [isMounted, setIsMounted] = useState(false)
  const portalElementRef = useRef(null)

  useEffect(() => {
    if (!enabled) {
      setIsMounted(false)
      return
    }

    // Find or create portal element
    let element = null

    if (id) {
      element = document.getElementById(id)
    }

    if (!element) {
      element = document.createElement('div')
      if (id) {
        element.id = id
      }
      element.setAttribute('data-portal', 'true')
    }

    const parentElement = parent ?? document.body
    const shouldAppend = !element.parentElement

    if (shouldAppend) {
      parentElement.appendChild(element)
    }

    portalElementRef.current = element
    setIsMounted(true)

    return () => {
      // Only remove if we created it (no id provided) and it was appended
      if (shouldAppend && !id && element && element.parentElement) {
        element.parentElement.removeChild(element)
      }
      portalElementRef.current = null
      setIsMounted(false)
    }
  }, [id, parent, enabled])

  const Portal = ({ children }) => {
    if (!isMounted || !portalElementRef.current) {
      return null
    }
    return createPortal(children, portalElementRef.current)
  }

  return {
    Portal,
    portalElement: portalElementRef.current,
    isMounted
  }
}

Key Features:

  • Creates portal containers dynamically
  • Reuses existing containers by ID
  • Automatic cleanup on unmount
  • Returns Portal component and mount status
  • Renders content outside component tree

API Reference

Options

id?: string

ID of the portal container element. If not provided, a new element will be created.

parent?: HTMLElement

Parent element to append the portal to (default: document.body)

enabled?: boolean

Whether to mount the portal immediately (default: true)

Returns

Portal: Component

Component to render content into the portal

portalElement: HTMLElement | null

The portal container element

isMounted: boolean

Whether the portal is mounted

💡 Use Cases

  • Modal dialogs that need to escape parent overflow
  • Tooltips that need to appear above all content
  • Toast notifications
  • Dropdown menus with complex positioning
  • Lightboxes and image galleries
  • Context menus