Moving DOM Nodes Without Losing State: The moveBefore() API
moveBefore() adds the DOM's missing move primitive: nodes relocate without the implicit remove-and-insert that resets iframes, focus, animations and dialogs.
Frontend code moves nodes around constantly — reordering a list, reparenting a video player into a picture-in-picture slot, hoisting a dialog to the end of <body>. Every one of those moves used to be a quiet lie, because the DOM had no move operation: "moving" really meant "remove, then insert"[5]. The moveBefore() method adds the missing primitive, and with it the ability to relocate a node without resetting the state it carries[1][5].
The Problem With "Moving" a Node
Chrome's launch post is blunt about the history: because the DOM Standard has only had remove and insert primitives, whenever you think you're moving something from one place to another, you're really removing and inserting under the hood[5]. For a <p> that is harmless, since neither operation has a disruptive side effect — but for a node that owns live state, the implicit removal is destructive[5].
The casualties are the parts of the platform that are stateful by design. MDN enumerates them: CSS animation and transition state, iframe loading state, interactivity states such as :focus and :active, fullscreen element state, the open/close state of popovers, and the modal state of <dialog> elements[3]. A <video> or <audio> element is the notable exception, because its play state survives removal and reinsertion regardless of the mechanism used[3].
What the Move Primitive Actually Does
moveBefore() is defined on the ParentNode mixin and takes the same shape as insertBefore(): the node to move, plus a reference child or null to append[1]. The spec describes it as moving a node "without first removing" it, and states plainly that the method preserves state associated with the moved node[1].
The mechanism is a third tree primitive alongside insert and remove[12]. The move algorithm performs the bookkeeping the other two share — reassigning slottables, adjusting live ranges, queueing mutation records — and then runs a separate moving steps hook for each shadow-including inclusive descendant of the moved subtree[1][12].
Crucially, because it is its own primitive, the move algorithm does not invoke the insertion steps or the removing steps for those descendants[1]. Those steps are where specifications tear state down — unloading an iframe document, closing a dialog, resetting an animation — so skipping them is what makes the move atomic in practice[1][12].
State That Survives, State That Doesn't
The design deliberately stops short of preserving everything. Live Range objects, and therefore text selection, are not carried across a move[13]. The Chromium intent-to-ship records that browser engineers jointly decided not to preserve live range state, and left the door open to revisiting it if authors ask[13]. If your UI depends on a selection surviving a reparent, that is the case to test first[13].
The Constraints You Have to Code Around
moveBefore() is not a drop-in replacement for insertBefore() at every call site, because it refuses to do anything it cannot do atomically[3]. MDN lists two hard constraints: it only works when moving a node within the same document, and it won't work if you try to move a node that is not connected to the DOM into an already connected parent, or vice versa[3]. In those cases it throws a HierarchyRequestError instead of falling back silently[3].
The spec frames the same rule as a precondition on the algorithm: if the new parent's shadow-including root differs from the moved node's, the move throws[1]. That has the side effect of ensuring a move is only performed if the parent and the node agree on connectedness[1]. Further checks reject a move that would place a node inside its own host-including inclusive ancestor, and throw NotFoundError when the reference child is not actually a child of the new parent[1].
A Safe Call Site
The practical answer is feature detection plus an explicit fallback, so unsupported browsers keep working and you know exactly which ones lose state[3][5].
const canMove = "moveBefore" in Element.prototype;
function reparent(node, newParent, ref = null) {
if (canMove) {
newParent.moveBefore(node, ref);
return;
}
// Fallback: state resets in browsers without the move primitive.
newParent.insertBefore(node, ref);
}For a framework or a DOM-morphing routine, the guard belongs at the boundary: attempt the atomic move, and fall back only when the environment cannot do it[3][5].
Custom Elements: Opting Into State-Preserving Moves
Custom elements need one more step, and it is the subtle part. By default, moving a custom element still fires disconnectedCallback() and then connectedCallback(), because the HTML Standard keeps that behaviour for compatibility with elements written before moveBefore() existed[2]. The consequence is that custom elements reset their state as if they were removed and re-inserted[2].
The opt-in is a new lifecycle callback: if an author implements connectedMoveCallback(), its mere existence supersedes the default behaviour of calling disconnectedCallback() and connectedCallback()[2]. The DOM algorithm enqueues that callback reaction for every custom element in the moved subtree when the new parent is connected[1]. MDN puts it simply — connectedMoveCallback() is run instead of connectedCallback() and disconnectedCallback()[3][4].
class VideoPanel extends HTMLElement {
#observer = new MutationObserver(() => this.#sync());
connectedCallback() {
this.setAttribute("tabindex", "0");
this.#observer.observe(this, { childList: true, subtree: true });
}
disconnectedCallback() {
this.#observer.disconnect();
}
// Runs instead of the connect/disconnect pair on a state-preserving move.
// The parent can change during the move, so re-derive ancestor-based state.
connectedMoveCallback() {
this.#sync();
}
}An empty connectedMoveCallback() is enough to stop the other two callbacks running; filling it in is where you re-run logic that depends on the element's new ancestors[2][4].
Support and Rollout
moveBefore() is a limited-availability feature rather than a baseline one[8]. Chrome 133 shipped it on 2025-02-04, Edge 133 followed on 2025-02-06, and Firefox 144 landed on 2025-10-14 on both desktop and Android[8]. Firefox's release notes list it as simply "Firefox now supports the Element.moveBefore API," and MDN's Firefox 144 notes confirm support on Element, DocumentFragment and Document[10][11].
Safari is the holdout. mdn/browser-compat-data records version_added: false for Safari with an implementation URL pointing at WebKit bug 281223, and the same shape for the connectedMoveCallback() lifecycle callback[6][7]. That bug is assigned and still open, and WebKit's standards position is marked support: positive, but there is no shipping implementation yet[9]. The web-features explorer tracks the practical effect: baseline availability has been blocked since October 2025 by Safari[8].
Treat moveBefore() as a progressive enhancement — feature-detect it, keep insertBefore() as the fallback path, and expect Safari to be the browser where your movable iframe still reloads[3][8].
Sources
[1] https://dom.spec.whatwg.org — DOM Standard (WHATWG) — move primitive, moveBefore() method steps, moving steps
[2] https://html.spec.whatwg.org/multipage/custom-elements.html — HTML Standard — Preserving custom element state when moved
[3] https://developer.mozilla.org/en-US/docs/Web/API/Element/moveBefore — MDN — Element: moveBefore() method
[4] https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements — MDN — Using custom elements (lifecycle callbacks and state-preserving moves)
[5] https://developer.chrome.com/blog/movebefore-api — Chrome for Developers — Preserve state during DOM mutations with moveBefore()
[6] https://github.com/mdn/browser-compat-data/blob/main/api/Element.json — mdn/browser-compat-data — api/Element.json (moveBefore)
[7] https://github.com/mdn/browser-compat-data/blob/main/api/CustomElementRegistry.json — mdn/browser-compat-data — api/CustomElementRegistry.json (connectedMoveCallback)
[8] https://web-platform-dx.github.io/web-features-explorer/features/move-before — Web features explorer — moveBefore() availability
[9] https://bugs.webkit.org/show_bug.cgi?id=281223 — WebKit Bugzilla 281223 — [DOM] Element.moveBefore() API
[10] https://www.mozilla.org/en-US/firefox/144.0/releasenotes — Firefox 144 release notes
[11] https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/144 — MDN — Firefox 144 for developers
[12] https://github.com/whatwg/dom/pull/1307 — whatwg/dom PR #1307 — Introduce moveBefore() state-preserving atomic move API
[13] https://groups.google.com/a/chromium.org/g/blink-dev/c/YE_xLH6MkRs — blink-dev — Intent to Ship: DOM moveBefore() method