How Hyperframes Handles HTML Composition Scoping: CSS and Script Isolation
Hyperframes isolates each HTML composition by rewriting CSS selectors with unique attribute prefixes and wrapping JavaScript in proxied IIFEs that restrict DOM queries to the composition root.
Hyperframes is an open-source framework developed by Heygen for building reusable HTML compositions. When rendering multiple compositions on a single page, HTML composition scoping prevents style leaks and script conflicts by transforming both the CSS and JavaScript at runtime.
CSS Scoping via Selector Rewriting
The scopeCssToComposition function in packages/core/src/compiler/compositionScoping.ts implements CSS isolation by parsing stylesheets and prefixing every selector with a composition-specific attribute selector.
PostCSS-Based Transformation
The function uses PostCSS to walk each CSS rule and rewrite selectors using scopeSelector. Every rule receives a prefix targeting [data-composition-id="…"], ensuring styles only apply within the composition's root element.
// packages/core/src/compiler/compositionScoping.ts
import { scopeCssToComposition } from './compiler/compositionScoping';
const originalCss = `
.title { color: red; }
#logo { width: 100px; }
`;
const compId = 'my-composition';
const scopedCss = scopeCssToComposition(originalCss, compId);
// => "[data-composition-id=\"my-composition\"] .title { … }"
// "[data-composition-id=\"my-composition\"] #logo { … }"
Global Rule Handling
The scoping mechanism normalizes special "root" selectors that refer to the composition container. It also removes selectors belonging to global at-rules such as @keyframes and @font-face, preserving essential global definitions while keeping element-specific rules scoped.
JavaScript Isolation Through Proxy Objects
Script isolation is handled by wrapScopedCompositionScript, which encapsulates composition JavaScript in an IIFE that creates proxy objects for document, window, and the Hyperframes API.
Scoped Document API
The wrapper defines a unique composition ID (__hfCompId) and builds a scoped document proxy that intercepts calls to querySelector, querySelectorAll, and getElementById. These calls are rewritten with __hfNormalizeSelector to automatically prepend the composition-specific attribute selector, then filtered using __hfContains to ensure they only return elements inside the composition root.
// packages/core/src/compiler/compositionScoping.ts
import { wrapScopedCompositionScript } from './compiler/compositionScoping';
const script = `
const title = document.querySelector('.title');
title.textContent = 'Scoped!';
`;
const wrapped = wrapScopedCompositionScript(script, 'my-composition');
// Returns an IIFE string that creates proxies for document/window
Scoped Window and Variable Management
A scoped window proxy redirects accesses to __timelines and other globals, enabling each composition to maintain its own GSAP timeline registry without collisions. The wrapper also injects per-composition variable tables (window.__hfVariablesByComp) before script execution, ensuring getVariables() returns only values belonging to the current composition.
Runtime Loading and Assembly
The compositionLoader.ts file orchestrates the scoping pipeline at runtime. It extracts the composition's root element, determines its data-composition-id, and processes each script payload through wrapScopedCompositionScript before appending it to the host DOM.
// Runtime integration example
import { wrapScopedCompositionScript } from './compiler/compositionScoping';
import { scopeCssToComposition } from './compiler/compositionScoping';
async function loadComposition(html: string, compId: string) {
// Parse HTML, extract <style> & <script> tags …
const scopedStyle = scopeCssToComposition(styleText, compId);
const wrappedScript = wrapScopedCompositionScript(scriptText, compId);
// Append style & script to the host element …
}
The loader stores declared variables in window.__hfVariablesByComp[compId] before the wrapped script executes. The getVariables.ts module exposes window.__hyperframes.getVariables(), which reads from this composition-specific store to provide isolated variable access.
Summary
- CSS Transformation:
scopeCssToCompositionrewrites selectors with[data-composition-id]prefixes using PostCSS, while preserving global at-rules. - Script Isolation:
wrapScopedCompositionScriptcreates proxieddocumentandwindowobjects that automatically scope DOM queries and global accesses to the composition root. - Runtime Coordination:
compositionLoader.tsinjects scoped styles and wrapped scripts, managing per-composition variable stores viawindow.__hfVariablesByComp.
Frequently Asked Questions
What is HTML composition scoping in Hyperframes?
HTML composition scoping is the mechanism that isolates individual HTML compositions so their styles, DOM queries, and side effects cannot interfere with other compositions on the same page. It combines CSS selector rewriting with JavaScript proxying to create deterministic boundaries around each composition instance.
How does Hyperframes prevent CSS conflicts between compositions?
According to the source code in packages/core/src/compiler/compositionScoping.ts, Hyperframes prevents CSS conflicts by prefixing every selector with a unique attribute selector targeting [data-composition-id="…"]. This ensures style rules only match elements inside the specific composition's root element, effectively sandboxing the CSS.
Can scripts in one Hyperframes composition access the DOM of another?
No. The wrapScopedCompositionScript function wraps each composition's JavaScript in an IIFE with proxied DOM APIs. The scoped document proxy intercepts querySelector and querySelectorAll calls, automatically prepending the composition-specific attribute selector and filtering results to stay within the composition root using __hfContains.
How are composition-specific variables managed in Hyperframes?
Variables are stored in a global object window.__hfVariablesByComp using the composition ID as the key. The runtime loader populates this store before script execution, and the getVariables() function exposed via window.__hyperframes reads only from the specific composition's entry, ensuring complete isolation between variable namespaces.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →