heygen-com/hyperframes · error · Error

drawElement: composition root has no parent node

Error message

drawElement: composition root has no parent node

What it means

Thrown during injectDrawElementCanvas() when the composition root element (matched by [data-composition-id]) has a null parentNode. The canvas injection flow needs a parent to insertBefore(canvas, root) and then reparent the root into the canvas. A detached root (not attached to document.body) has no parent and cannot be wrapped.

Source

Thrown at packages/engine/src/services/drawElementService.ts:223

  height: number,
): Promise<void> {
  await page.evaluate(
    ({ w, h }: { w: number; h: number }) => {
      const root = document.querySelector("[data-composition-id]") as HTMLElement | null;
      if (!root || document.getElementById("__hf_de_canvas")) return;
      // Record the root's base opacity (timeline at 0, before any entrance/
      // outro tween) for the LEGACY root-opacity ratio correction. The
      // correction only runs on paths whose paint does not bake the root's
      // current opacity into the snapshot — BeginFrame (sync=false) captures
      // and builds without canvas.requestPaint(); see __hfDeInvalidate below.
      try {
        (window as unknown as { __HF_ROOT_BASE_OPACITY__?: number }).__HF_ROOT_BASE_OPACITY__ =
          parseFloat(getComputedStyle(root).opacity) || 1;
      } catch {
        /* leave undefined → ratio defaults to 1 */
      }
      const parent = root.parentNode;
      if (!parent) throw new Error("drawElement: composition root has no parent node");
      const canvas = document.createElement("canvas") as HTMLCanvasElement & {
        layoutsubtree: boolean;
      };
      canvas.id = "__hf_de_canvas";
      canvas.setAttribute("layoutsubtree", "");
      canvas.width = w;
      canvas.height = h;
      canvas.style.cssText = "display:block;position:absolute;top:0;left:0;z-index:0";
      parent.insertBefore(canvas, root);
      canvas.appendChild(root);
      // Invalidation sentinel: a canvas child OUTSIDE the captured root.
      // Toggling its background each capture is a PAINT-level dirty
      // (layout/transform toggles do NOT fire the canvas `paint` event), so a
      // paint — and a fresh snapshot — is guaranteed even for static frames,
      // without the sentinel ever appearing in drawElementImage(root) output.
      const tick = document.createElement("div");
      tick.id = "__hf_de_tick";
      tick.style.cssText =

View on GitHub (pinned to c2996c8626)

Solutions

  1. Ensure injectDrawElementCanvas is called after the composition root is in the live DOM: wait for document.querySelector('[data-composition-id]') to return a node whose parentNode is non-null.
  2. Use page.waitForSelector('[data-composition-id]') before calling injectDrawElementCanvas.
  3. Verify the composition HTML template includes the root as a child of body (not a detached fragment).
  4. If using shadow DOM, adjust the querySelector to pierce it or attach the canvas inside the shadow root.

Example fix

// before
await page.goto(url, { waitUntil: 'domcontentloaded' });
await injectDrawElementCanvas(page, w, h);

// after
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-composition-id]');
await injectDrawElementCanvas(page, w, h);
Defensive patterns

Strategy: validation

Validate before calling

// before calling injectDrawElementCanvas:
const hasRoot = await page.evaluate(() => {
  const root = document.querySelector('[data-composition-id]');
  return root !== null && root.parentNode !== null;
});
if (!hasRoot) {
  throw new Error('Composition root not attached to DOM');
}

Try / catch

try {
  await injectDrawElementCanvas(page, w, h);
} catch (err) {
  if (err instanceof Error && err.message.includes('no parent node')) {
    // wait for DOM and retry
    await page.waitForSelector('[data-composition-id]');
    await injectDrawElementCanvas(page, w, h);
  }
  throw err;
}

Prevention

When it happens

Trigger: injectDrawElementCanvas(page, width, height) calls page.evaluate() which queries [data-composition-id], finds it, checks document.getElementById('__hf_de_canvas') is absent, then reads root.parentNode. If the root was created but never appended to the document (or was removed), parentNode is null.

Common situations: The composition HTML loaded but the root element was removed by a script before injection ran. injectDrawElementCanvas was called before the page finished loading the composition DOM (race condition with page.goto). A custom composition template doesn't attach the root to document.body. The root was moved to a shadow DOM.

Related errors


AI-assisted analysis of heygen-com/hyperframes@c2996c8626 (2026-08-12). Data as JSON: /api/errors/5f0022b896fd6766. Report an issue: GitHub.