Composition scope shim doesn't scope array selector targets: tweens leak across compositions (element stays visibility:hidden for a whole beat)
Problem
In a multi-composition single-file build, the injected composition-scoping shim only filters string targets. An array of selector strings is handed to GSAP untouched and resolved against the whole document, so a set/to/from/fromTo in one composition animates elements belonging to every other composition.
Root cause
The wrapper injected per composition (__hfScopeTimeline / __hfScopedGsap / __hfRun):
var __hfResolveGsapTarget = function(target) {
if (typeof target !== "string") return target; // arrays/objects bypass scoping
return __hfQueryAll(target);
};
...
timeline[method] = function(target) {
var args = Array.prototype.slice.call(arguments);
args[0] = __hfResolveGsapTarget(target);
return original.apply(timeline, args);
};__hfQueryAll -> __hfContains correctly clips string selectors to the composition root, but a non-string is returned as-is, so GSAP's own target resolution runs globally. gsap.utils.toArray(arrayOfSelectors) has the same gap (the proxy's utils.toArray hook calls the same resolver).
Steps to reproduce
8-composition project (beat-1 .. beat-8), each hosted as <div data-composition-id="beat-N">. In compositions/beat-1.html:
const tl = gsap.timeline({ paused: true });
tl.set([".bg-grid", ".sub"], { autoAlpha: 0 }, 0);Then hyperframes snapshot . --at <t> --no-end, and in the built single file:
gsap.getTweensOf(document.querySelector('[data-composition-id="beat-7"] .sub'))[0].targets().length
// => 26 (every element matching any selector in the array, document-wide)The 26 targets include beat-7's own .bg-grid, .bg-glow and all three .sub elements. Expected: only beat-1's elements.
Expected behavior
An array target is resolved (and clipped) per element, exactly like the equivalent comma-separated string selector.
Actual behavior
Tweens leak across composition boundaries.
The failure mode is asymmetric and silent, which is what makes it nasty:
- If the leaked-to composition also animates that element with its own correctly-scoped string tween, the leak is overwritten and nothing is visible — most authors would never notice.
- If it does not animate it (the documented "CSS = end state, opacity only in JS" pattern encourages leaving static elements alone), the leaked
autoAlpha: 0sticks for the entire timeline, i.e.visibility: hiddenon that element and its whole subtree. Rendered frames show a blank region, yetgetComputedStyle()on descendants still reportsopacity: 1, bounding rects are intact anddocument.elementFromPoint()hits the element — so DOM inspection says everything is fine.
In my case beat-1's tl.set([... ".sub" ...], { autoAlpha: 0 }, 0) (at t=0, with immediateRender) hid the three .sub wrappers in beat-7, whose mid + bottom content lives underneath them. hyperframes snapshot and the exported video.mp4 show that beat's whole mid/bottom band empty (ink coverage 0.00% over ~24s) while the top region renders normally. hyperframes validate / check report 0 errors, inspect reports 0 issues, and no runtime error is logged, because nothing is actually broken in CSS, GSAP or the timeline — only the scope filter is bypassed.
Verify by forcing the ancestor back on at a time its content should be visible:
window.__player.enableRenderMode();
window.__player.renderSeek(178.736);
// mid band ink 0.32%, bottom 0.00%
window.gsap.set(document.querySelector('[data-composition-id="beat-7"]').querySelectorAll(".sub"), { autoAlpha: 1 });
// mid band ink 5.61%, bottom 1.07%(Values are the fraction of pixels above a 90-luminance threshold in fixed top/mid/bottom bands of a 1080x1260 frame, measured from page.screenshot() in headless Chrome against the compiled single-file HTML, so this is not snapshot-path specific.)
Suggested fix
var __hfResolveGsapTarget = function(target) {
if (Array.isArray(target)) return target.flatMap(__hfResolveGsapTarget);
if (typeof target !== "string") return target;
return __hfQueryAll(target);
};Workaround
Use one comma-separated string selector, which goes through __hfQueryAll and the __hfContains clip:
tl.set(".bg-grid, .sub", { autoAlpha: 0 }, 0);A lint rule flagging selector arrays in gsap/timeline calls would also have caught this before render.
Environment
hyperframes 0.8.41 (global npm install, npx)
Node.js v24.17.0 (darwin arm64, macOS)
Chrome 152 (headless, via puppeteer-core) for the DOM/pixel probesHappy to put together a minimal 2-composition fixture repo if that helps.
Source: heygen-com/hyperframes