import { ReactNode, useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
interface PortalProps {
children: ReactNode
container?: Element
}
export function Portal({ children, container }: PortalProps) {
const [mountNode, setMountNode] = useState<Element | null>(null)
useEffect(() => {
setMountNode(container || document.body)
}, [container])
return mountNode ? createPortal(children, mountNode) : null
}
import { useState, useRef, ReactNode } from 'react'
import { Portal } from './Portal'
interface TooltipProps {
content: ReactNode
children: ReactNode
}
export function Tooltip({ content, children }: TooltipProps) {
const [isVisible, setIsVisible] = useState(false)
const [position, setPosition] = useState({ top: 0, left: 0 })
const triggerRef = useRef<HTMLDivElement>(null)
const updatePosition = () => {
if (triggerRef.current) {
const rect = triggerRef.current.getBoundingClientRect()
setPosition({
top: rect.bottom + window.scrollY + 8,
left: rect.left + window.scrollX + rect.width / 2,
})
}
}
return (
<>
<div
ref={triggerRef}
onMouseEnter={() => {
updatePosition()
setIsVisible(true)
}}
onMouseLeave={() => setIsVisible(false)}
className="inline-block"
>
{children}
</div>
{isVisible && (
<Portal>
<div
className="absolute z-50 px-3 py-2 text-sm text-white bg-gray-900 rounded shadow-lg"
style={{
top: `${position.top}px`,
left: `${position.left}px`,
transform: 'translateX(-50%)',
}}
>
{content}
</div>
</Portal>
)}
</>
)
}
Portals render components outside their parent DOM hierarchy while maintaining React's component tree for context and events. I use portals for modals, tooltips, and dropdowns that need to escape overflow: hidden containers or z-index stacking contexts. ReactDOM.createPortal takes a component and a DOM node, rendering the component as a child of that node. Events bubble through the React tree, not the DOM tree, so click handlers work naturally. For modals, I render into a dedicated div at document root. Tooltips portal into a positioned container to avoid clipping. This technique solves CSS positioning nightmares while keeping component logic clean.