Repository navigation
Description
Describe the bug
When a sub-composition's internal GSAP timeline runs shorter than its host's data-duration, the slot renders correctly during the timeline window, then goes black for the rest of the slot. The runtime's own skill rule says data-duration takes precedence over GSAP timeline length — so the sub-comp should hold its final timeline state through the full slot.
Root compositions hold correctly. Only sub-compositions loaded via data-composition-src exhibit this.
Link to reproduction
Steps to reproduce
- Clone the repo above.
npx hyperframes render --fps 60 --quality high --output repro.mp4- Sample frames across the 3s output:
for t in 0.5 1.0 1.5 2.0 2.5 2.9; do ffmpeg -y -loglevel error -ss $t -i repro.mp4 -frames:v 1 frame-${t}.png done
- Open each PNG.
The repo contains exactly these two files:
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style>
html, body { margin: 0; padding: 0; background: #111; }
#root { position: relative; width: 1920px; height: 1080px; background: #111; }
.scene { position: absolute; inset: 0; }
</style>
</head>
<body>
<div
id="root"
data-composition-id="root"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="3"
>
<!-- Host slot is 3s. Sub-comp's internal GSAP timeline runs only 0–1s. -->
<div
id="sub-host"
class="clip scene"
data-composition-id="sub"
data-composition-src="compositions/sub.html"
data-start="0"
data-duration="3"
data-track-index="1"
></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<script>
const tl = gsap.timeline({ paused: true });
tl.set("#sub-host", { opacity: 1 }, 0);
window.__timelines = window.__timelines || {};
window.__timelines["root"] = tl;
</script>
</body>
</html>compositions/sub.html
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8" /></head>
<body>
<template id="sub-template">
<div data-composition-id="sub" data-width="1920" data-height="1080">
<style>
.sub-root {
position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center;
background: #1d2540;
color: #f8f4e8;
font: 600 200px/1 system-ui, sans-serif;
}
</style>
<div class="sub-root"><div class="sub-text">HOLD ME</div></div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<script>
(function () {
const tl = gsap.timeline({ paused: true });
// Entrance lands at t=1.0s. No further tweens.
tl.fromTo(
".sub-text",
{ y: 80, opacity: 0 },
{ y: 0, opacity: 1, duration: 1.0, ease: "power3.out" },
0,
);
window.__timelines = window.__timelines || {};
window.__timelines["sub"] = tl;
})();
</script>
</div>
</template>
</body>
</html>Expected behavior
The sub-composition holds its final timeline state through the full 3s host slot. At every sampled timestamp (0.5s through 2.9s), the rendered frame shows the navy background and "HOLD ME" text — mid-entrance at 0.5s, fully composed from 1.0s onward.
This matches the runtime's documented rule (see "Additional context" below).
Actual behavior
The sub-composition is visible during 0–1s, then the slot goes black for the rest:
| t | Result |
|---|---|
| 0.5s | ✅ Text mid-entrance |
| 1.0s | ❌ Black |
| 1.5s | ❌ Black |
| 2.0s | ❌ Black |
| 2.5s | ❌ Black |
| 2.9s | ❌ Black |
The slot's #1d2540 background (on .sub-root inside the sub-composition) also disappears — only the root container's #111 shows through. The sub-composition DOM stops rendering, not just the GSAP-animated elements.
Environment
✓ Version 0.6.14 (latest)
✓ Node.js v24.14.1 (darwin arm64)
✓ FFmpeg ffmpeg version 8.1.1
✓ Chrome cache: chrome-headless-shell mac_arm-147.0.7727.57Additional context
What the docs say
The skill documentation that ships with the package (dist/skills/hyperframes/SKILL.md) lists this as a non-negotiable rule under "Timeline Contract":
Duration comes from
data-duration, not from GSAP timeline length
And under "Composition Clips":
data-duration... Takes precedence over GSAP timeline duration
Root compositions honor the rule. Sub-compositions don't.
Workaround
Extending the sub-composition's GSAP timeline past the host's data-duration keeps the slot visible. Any tween whose end falls at t ≥ duration works — even a no-op tween between identical values:
// Inside the sub-composition's timeline, after the real entrance:
tl.to(".sub-text", { opacity: 1, duration: 2.0 }, 1.0);With this added to the repro, all six sample timestamps render correctly. The entrance state recovers fine — the runtime simply stops seeking into it after the timeline ends.
Possible fixes
- Runtime: if the GSAP adapter clamps
seek(localTime)totimeline.duration(), clamp to the host'sdata-durationinstead — or calltl.progress(1)oncelocalTime >= timeline.duration(). Either holds the end state. - Docs: if internal timeline length is intentionally authoritative for sub-comps, the "data-duration takes precedence" rule needs the exception called out, and the workaround needs a name.
Activity
miguel-heygen commentedon May 17, 2026
Should be fixed by #917
fix(runtime): hold external sub-compositions in render mode
miguel-heygen commentedon May 17, 2026
Follow-up fix landed in
2b46565c. PR #917 only fixed the preview path — the render path was still broken because the producer'shtmlCompiler.tsstripsdata-composition-srcduring inlining without settingdata-composition-file(the core bundler sets both). The runtime now checks both attributes, and the producer mirrors the core bundler's behavior.Verified with the exact repro from this issue — all 6 timestamps now render correctly in both preview and render modes.
refactor: extract shared inlineSubCompositions from bundler and producer
feat(lint): warn when a sub-composition slot blanks before the host
feat(lint): warn when a sub-composition slot blanks before the host
feat(lint): warn when a sub-composition slot blanks before the host (#…
fix(runtime): hold external sub-compositions in render mode
refactor: extract shared inlineSubCompositions from bundler and producer
feat(lint): warn when a sub-composition slot blanks before the host (h…