Reparent

Move rendered content to a new place in the DOM without unmounting it. React state, DOM state, and iframes survive the move.

Usage

Render content once with ReparentContent, place it with a ReparentSlot. When the slot moves (a different parent, a swapped layout, dialog to drawer), the content's React subtree never unmounts, and where the browser supports moveBefore (Chrome/Edge 133+, Firefox 144+) DOM state moves with it: iframes don't reload, focus, scroll, and animations are kept. Without moveBefore (Safari) the node is re-appended: React state still survives, an iframe reloads.

import {
  ReparentContent,
  ReparentSlot,
  supportsMoveBefore,
  useReparent,
} from '@epilot/spark-ui/reparent'
function Layout({ sidebar }: { sidebar: boolean }) {
  const handle = useReparent()

  return (
    <>
      {sidebar ? (
        <aside>
          <ReparentSlot handle={handle} />
        </aside>
      ) : (
        <main>
          <ReparentSlot handle={handle} />
        </main>
      )}
      <ReparentContent handle={handle}>
        <VideoPlayer />
      </ReparentContent>
    </>
  )
}

If the move happens because React unmounts the current slot's whole subtree (swapping a dialog for a drawer), call handle.park() before the state update that triggers the swap: React tears down the old tree ancestors-first, so waiting for an unmount cleanup is too late. See the full pattern in the Responsive Dialog iframe example.

API Reference

ExportDescription
useReparent()Creates the persistent host. Returns { host, park }
ReparentContentRenders children into the host exactly once; takes handle
ReparentSlotA div that adopts the host on mount; takes handle plus any div props
handle.park()Moves the host to safety (hidden, on document.body); call before a state update that unmounts the current slot
supportsMoveBeforeWhether this browser preserves DOM state on move; use it to lock layout swaps for iframes where it is false

On this page