
Hyperframes Core
- 228k installs
- 39.5k repo stars
- Updated August 5, 2026
- heygen-com/hyperframes
hyperframes-core is the HyperFrames composition engine defining HTML-native video composition with deterministic frame-accurate rendering.
About
Define deterministic video compositions using HTML with data attributes for timing, tracks, and seekable animations. The skill provides the composition engine with frame-accurate rendering, media playback, and CI-friendly testing.
- HTML-native composition contract with data attributes for timing and tracks
- Deterministic rendering - same input produces identical frames for CI and testing
- Framework-owned media playback, variables, and seekable animation support
Hyperframes Core by the numbers
- 227,845 all-time installs (skills.sh)
- +38,713 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #42 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
hyperframes-core capabilities & compatibility
- Capabilities
- composition definition · deterministic rendering · timing control
- Use cases
- video generation
npx skills add https://github.com/heygen-com/hyperframes --skill hyperframes-coreAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 228k |
|---|---|
| repo stars | ★ 39.5k |
| Security audit | 2 / 3 scanners passed |
| Last updated | August 5, 2026 |
| Repository | heygen-com/hyperframes ↗ |
How do you structure a valid HyperFrames HTML video composition?
Create deterministic video compositions with HTML-native timing and animation support for reliable video generation.
Who is it for?
Building deterministic video pipelines and compositions requiring frame accuracy and testing support.
Skip if: Pure CSS marketing sites, non-HyperFrames video editors, or motion-only tasks without composition scaffolding.
When should I use this skill?
Creating HyperFrames compositions with exact timing and deterministic output
What you get
Minimal renderable HTML compositions with clips, tracks, data-* timing, variables, and validated structure.
- Composition HTML files
- Validated project structure
- Clip and track definitions
Files
HyperFrames Core
HyperFrames renders video from HTML. A composition is an HTML file whose DOM declares timing with data-* attributes, whose animation runtime is seekable, and whose media playback is owned by the framework.
This skill is the technical contract. Other concerns live in sibling skills:
hyperframes-animation— atomic motion rules + scene blueprints + per-runtime adapters (GSAP, Lottie, Three, Anime.js, CSS, WAAPI, TypeGPU)hyperframes-creative— palettes, typography, narration, beat planning, audio-reactivehyperframes-media— TTS, Whisper transcribe, background removal, captionshyperframes-registry— install blocks and components (hyperframes add)hyperframes-cli— dev loop commands (lint / validate / preview / render)
For Tailwind v4 projects (hyperframes init --tailwind), see references/tailwind.md — the browser-runtime contract is distinct from Studio's Tailwind v3 setup.
Routing
| Want to… | Read |
|---|---|
| See a minimal renderable composition | references/minimal-composition.md |
| Decide between monolithic single-file and modular sub-comp architecture | references/composition-patterns.md |
Structure a modular index.html (orchestrator + slots + root audio) | references/composition-patterns.md |
| Pick a sub-comp archetype (content / driver-only / multi-scene merge / audio-reactive) | references/composition-patterns.md |
Look up a data-* attribute (root, clip, sub-comp host, legacy aliases) | references/data-attributes.md |
Use class="clip" correctly | references/data-attributes.md |
Pick a data-track-index; same-track overlap; track-index vs CSS z-index | references/tracks-and-clips.md |
Time a clip relative to another (data-start="intro + 2", crossfade overlap, chains) | references/tracks-and-clips.md |
Wire a sub-composition (host attributes, <template> wrapper, per-instance variables) | references/sub-compositions.md |
Animate inside a sub-composition (gsap.fromTo over gsap.from, seek-back behavior) | references/sub-compositions.md |
Declare variables (types, extra options, defaults, --strict-variables in CI) | references/variables-and-media.md |
Place <video> / <audio>, set volume, trim with data-media-start | references/variables-and-media.md |
Build a seekable timeline (paused, sync construction, gsap.set later-scene trap) | references/determinism-rules.md |
Avoid non-deterministic state (clocks, Math.random, repeat: -1, finite repeat formula) | references/determinism-rules.md |
Know what can / cannot be animated (visual-property allowlist; not display/visibility) | references/determinism-rules.md |
Fit text and prevent overflow (fitTextFontSize signature, <br> rule, layout contract) | references/determinism-rules.md |
| Author full-frame motion with shared backgrounds | references/full-screen-motion.md |
Work in a Tailwind v4 project (init --tailwind) | references/tailwind.md |
For animation runtime specifics (GSAP API, Lottie, Three.js, etc.) go to hyperframes-animation → adapters/<runtime>.md.
Composition Structure
Two root forms; they are not interchangeable.
- Standalone (the top-level
index.html) — root<div data-composition-id="…">sits directly in<body>. No `<template>` wrapper. Wrapping a standalone root in<template>hides all content from the browser and breaks rendering. - Sub-composition (a file loaded via
data-composition-src) — root<div data-composition-id="…">must be wrapped in<template>. Without the wrapper the runtime cannot extract and mount it.
⚠ Sub-composition transport rule: the runtime only clones `<template>` contents into the live DOM. Everything outside the template — including<head>and any<style>/<script>/<link>that lives in<head>— is discarded. Put<style>and<script>blocks inside<template>, not in<head>.
>
⚠ Host-id rule: in the host file,data-composition-idon the slot must exactly equal the inner template'sdata-composition-idand thewindow.__timelines["<id>"]key. Do not add-mount/-slot/-hostsuffixes.
See references/sub-compositions.md for the file shape, host wiring, pitfall examples, and the pre-render verification checklist.
Root must be sized (silent layout bug)
The standalone root must establish an explicitly sized box, and every ancestor between it and a height: 100% element must have a resolved height. If the root or an intermediate wrapper has no height, a flex/height:100% content container collapses to ~0 and content piles into the top-left corner (often clipping the first glyph at x=0). lint, validate, and inspect do not catch this — inspect substitutes the authored data-width/data-height for a collapsed root and reports "0 layout issues."
<body style="margin: 0">
<div
id="root"
data-composition-id="main"
data-width="1920"
data-height="1080"
data-duration="5"
style="position: relative; width: 1920px; height: 1080px; overflow: hidden"
>
<!-- Center robustly: position:absolute + inset:0 fills the sized root regardless of
intermediate wrappers; or use a flex container ONLY if its parent chain is sized. -->
<section
class="clip"
data-start="0"
data-duration="5"
data-track-index="1"
style="position: absolute; inset: 0; display: grid; place-items: center; padding: 120px 160px; box-sizing: border-box"
>
<h1>Title</h1>
</section>
</div>
</body>Keep the padding (≥80px) on the centering container — it is the title-safe margin that stops large type touching the frame edge. See references/minimal-composition.md.
Timeline Contract (GSAP default)
Every composition registers exactly one GSAP timeline.
- Create with
gsap.timeline({ paused: true })— the player owns playback. - Register at
window.__timelines["<composition-id>"]; the key must exactly match the root'sdata-composition-id. Dot syntax (window.__timelines.<id> = tl) is equivalent when the id is a valid identifier; use brackets if the id contains-. - Build the timeline synchronously during page load — not inside
async,setTimeout,Promise, or event handlers. The renderer samples after page load completes; any deferred timeline construction misses the sample. - Render duration comes from
data-durationon the root, not from GSAP timeline length. Do not pad the timeline with empty tweens to set duration. - For sub-compositions, do not manually nest sub-timelines into the host (
master.add(sub)); the framework drives them independently.
For non-GSAP runtimes (Lottie / Three / WAAPI / CSS / Anime.js / TypeGPU), the equivalent contract lives in hyperframes-animation/adapters/<runtime>.md. See references/determinism-rules.md for the cross-runtime Animation Runtime Contract.
Non-Negotiable Rules
These break the renderer — or produce silent visual bugs that `lint`/`validate`/`inspect` do NOT catch (rules 3, 7-8). (Synchronous timeline construction is covered above in Timeline Contract.)
1. No Math.random() / Date.now() / performance.now() driving visuals — use a seeded PRNG. 2. No repeat: -1. Use repeat: Math.max(0, Math.floor(duration / cycleDuration) - 1) — `floor`, not `ceil` (ceil overshoots data-duration and trips the gsap_repeat_ceil_overshoot lint; max(0, …) avoids a negative repeat = infinite). 3. <video>/<audio> must be a direct child of the host root (index.html) — never inside a sub-comp <template>, never inside a wrapper <div>; otherwise it is never decoded and renders blank/black (silent). The framework owns playback: no video.play() / audio.play() / currentTime = …. A sub-comp cannot drive host elements (selector or querySelector), so animate host media from the main timeline at global time. See references/variables-and-media.md. 4. No gsap.set() on clip elements from later scenes (they are not in the DOM yet). Use tl.set(selector, vars, time) at or after the clip's data-start. 5. No animating display / visibility. Animate opacity / transforms; the clip lifecycle handles show/hide. 6. No <br> in body text. Let text wrap via max-width. 7. Transformed elements must be block-level + sized. transform/scaleX/scaleY is a no-op on an inline `<span>`, and scaling an auto-width (0px) element shows nothing → invisible bars/fills. Give them display: block/inline-block/flex-item and a real width/height (e.g. width: 100% inside a sized parent). 8. Absolutely-positioned decoratives (badges, pills, tags) that pulse or overshoot (yoyo scale, back.out) need clearance at their peak size and must not straddle an overflow: hidden edge — else they overlap a neighbor or get clipped. Position for the largest frame, not the resting one.
Editing Existing Compositions
- Read the actual files before editing.
- Preserve unrelated timing, tracks, IDs, variables, and media paths.
- Match existing composition IDs and timeline registry keys.
- When adding a clip, choose a non-overlapping
data-track-indexor intentionally adjust surrounding timing. - When adding a sub-composition, verify its internal
data-composition-idbefore wiring the host.
Validation
Use hyperframes-cli for command details.
- [ ]
npx hyperframes lintpasses (0 errors) - [ ]
npx hyperframes validatepasses (0 console errors) - [ ]
npx hyperframes inspectpasses, or overflow is intentionally marked - [ ] Projects with sub-compositions:
npx hyperframes snapshot --at <midpoints>and eyeball each frame
Composition Patterns
How to architect a project — when to inline everything in one HTML, when to split into sub-compositions, what the index.html orchestrator looks like at scale, and the common sub-composition archetypes seen in real projects. Pair with minimal-composition.md (single-file shape) and sub-compositions.md (mechanics of a sub-comp file).
Two Architectures
| Monolithic (single file) | Modular (sub-compositions) | |
|---|---|---|
| Project layout | index.html only | index.html + compositions/<scene>.html per scene |
| Where scenes live | Inline <section class="clip"> siblings under the root | Each scene is a separate file wrapped in <template> |
| Timeline registration | One timeline keyed at the root's data-composition-id | Root timeline (often near-empty) + one timeline per sub-comp, each keyed by its id |
| Routing entry | references/minimal-composition.md | references/sub-compositions.md |
Both architectures use the same runtime contract — data-* attributes + window.__timelines[id]. The choice is structural, not behavioral.
Pick monolithic when
- The whole video is one continuous scene with no hard cuts.
- Scenes share heavy state (one canvas/WebGL context spanning the whole video, a single SVG that morphs across all beats).
- Total scope is small (~200–400 lines of markup + script).
- No scene is reused across projects.
Pick modular when
- The video has clear scene cuts — each scene is its own segment of the timeline.
- Some scenes are large (>100 lines of markup or significant scripted animation).
- A scene is reusable (kinetic intro, end-card logo lockup, a transition).
- The video has a continuous audio track over multiple visual segments. Keep audio at the root, visual segments as sub-comps.
- You want to author/iterate on scenes in isolation (preview a single sub-comp file directly).
Refactor between them
Conversion is mechanical and reversible. To lift a monolithic scene into a sub-comp: wrap the scene's markup + scoped CSS + its slice of the parent timeline into a <template>, save as compositions/<scene>.html, replace the inline content in index.html with a slot <div data-composition-src="compositions/<scene>.html">, and have the sub-comp register its own timeline at window.__timelines["<scene>"]. The parent timeline shrinks accordingly.
If a monolithic project is approaching three or more scene cuts, prefer modularizing _before_ adding the next scene. Mixed projects where some scenes are inline and siblings are in compositions/ are the hardest to maintain.
Modular Orchestrator Pattern
When using sub-compositions, index.html should be thin. Its job is to declare slots, lay them out in time, mount the audio track, and register a (usually empty) root timeline. All scene animation lives inside the sub-comps.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
body {
margin: 0;
background: #000;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
}
/* Sub-comp slots stretch to fill the root. */
[data-composition-id="root"] > div[data-composition-src] {
position: absolute;
inset: 0;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="root"
data-width="1920"
data-height="1080"
data-duration="30"
>
<!-- Sequential scenes — each one a sub-composition slot. -->
<div
id="el-intro"
data-composition-id="intro"
data-composition-src="compositions/intro.html"
data-start="0"
data-duration="6"
data-track-index="1"
></div>
<div
id="el-body"
data-composition-id="body"
data-composition-src="compositions/body.html"
data-start="6"
data-duration="18"
data-track-index="1"
></div>
<div
id="el-outro"
data-composition-id="outro"
data-composition-src="compositions/outro.html"
data-start="24"
data-duration="6"
data-track-index="1"
></div>
<!-- Continuous audio at the root — survives scene cuts. -->
<audio
id="el-bgm"
src="assets/bgm.mp3"
data-start="0"
data-duration="30"
data-track-index="10"
data-volume="0.6"
></audio>
</div>
<script>
window.__timelines = window.__timelines || {};
window.__timelines["root"] = gsap.timeline({ paused: true });
</script>
</body>
</html>Key properties of this layout:
- Visual scenes on the same `data-track-index` (e.g.
1). Sequential — they cannot overlap on the same track. For a cross-fade between two scenes, put one on a higher track and overlap their times by the fade duration. - Audio on a separate, higher track index (e.g.
10). Keeps the linter's overlap rules clear of any visual collisions. - Root timeline is near-empty. All animation lives in the sub-comps. A root-level fade-to-black at the very end is fine; do not stage a parallel animation track from the root.
- Host slot ids use
el-<name>or<scene-id>. The slot'sdata-composition-idmust still equal the sub-comp's internal id (seesub-compositions.md).
Sub-Composition Archetypes
A. Content scene (default)
The sub-comp contains the scene's full DOM, scoped CSS, and timeline. This is the standard pattern in sub-compositions.md — most scenes are this.
B. Host media + main-timeline driver (REQUIRED for any <video>/<audio>)
Media playback only works when the <video>/<audio> is a direct child of the host root — never inside a sub-comp <template> (it would render blank/black). This is not optional or "for media that spans scenes"; it applies to every clip, including a scene-specific one. The scene's sub-comp keeps the frame/shell; the media is a host sibling positioned over it.
A sub-comp timeline cannot drive host elements (a global selector or document.querySelector does not resolve across the boundary). So author the media's per-scene motion (scale/opacity/morph/tilt/breathing) on the main timeline in index.html, at global time = scene-local time + the scene slot's data-start.
<!-- index.html (host) -->
<div
id="el-final"
data-composition-id="final-anim"
data-composition-src="compositions/final-anim.html"
data-start="20"
data-duration="6"
data-track-index="1"
></div>
<!-- media is a DIRECT root child; sits over the sub-comp's frame -->
<video
id="final-video"
class="clip"
src="assets/final.mp4"
data-start="20"
data-duration="6"
data-track-index="2"
muted
playsinline
style="position:absolute; left:360px; top:100px; width:1200px; height:680px; object-fit:cover; border-radius:24px;"
></video>
<script>
// MAIN timeline drives the host video. Global time: scene starts at 20.
window.__timelines = window.__timelines || {};
const main = window.__timelines["main"];
main.fromTo(
"#final-video",
{ scale: 1.4, filter: "blur(14px)" },
{ scale: 1.0, filter: "blur(0px)", duration: 0.9, ease: "power3.out" },
20,
); // = slot data-start (+ any scene-local offset)
</script>
<!-- compositions/final-anim.html — frame/shell only, no <video>, no host-element animation -->
<template>
<div
data-composition-id="final-anim"
data-width="1920"
data-height="1080"
data-duration="6"
style="position:absolute; inset:0; pointer-events:none;"
>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// animate ONLY this sub-comp's own elements here (labels, frame, overlays)
window.__timelines["final-anim"] = tl;
</script>
</div>
</template>Caveats:
- The host media must be a direct root child and exist in the DOM (static in
index.html) — it always is. - Clip lifecycle owns the media element's visibility across its
[data-start, data-start+data-duration]window. The main-timeline opacity/scale tweens compose with it fine; for an opacity reveal/crossfade prefer a host wrapper so you are not fighting the lifecycle on the media element itself. - Two media elements sharing the same
src+data-starttriggerduplicate_media_discovery_risk(benign — both still render).
C. Multi-scene merge
When several beat-level scenes share continuous state — a chat thread that grows, a persistent headline word that carries across the cut, a single canvas with internal phase changes — collapse them into one sub-comp and use internal phase divs rather than multiple sub-comp slots.
<!-- compositions/act2-merged.html -->
<template>
<div data-composition-id="act2-merged" data-width="1920" data-height="1080" data-duration="9">
<style>
[data-composition-id="act2-merged"] .phase {
position: absolute;
inset: 0;
opacity: 0;
}
</style>
<div class="phase" id="phase-a">…</div>
<div class="phase" id="phase-b">…</div>
<div class="phase" id="phase-c">…</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.set("#phase-a", { opacity: 1 }, 0);
tl.to("#phase-a", { opacity: 0, duration: 0.4 }, 3.0);
tl.set("#phase-b", { opacity: 1 }, 3.0);
// …
window.__timelines["act2-merged"] = tl;
</script>
</div>
</template>Reach for this over multiple sequential slots when scenes share DOM, share a canvas, or need to cross-fade with persistent elements (a headline that survives the cut between phases). Each phase is just a div inside the same sub-comp — the parent timeline never has to know about the internal phase boundaries.
D. Audio at root, reactive visual inside
Audio always lives at the host (index.html) as a root-level <audio> so playback survives scene cuts. A sub-comp that visualizes audio should read a pre-baked frequency curve at init, then sample the baked curve from its timeline — the visual must still be a deterministic function of tl.time(), not of audio.currentTime. See determinism-rules.md and hyperframes-creative for the authoring pattern.
Naming Conventions
| Thing | Convention | Example |
|---|---|---|
| Sub-comp file | compositions/<scene-id>.html | compositions/act0-intro-bell.html |
Sub-comp <template> id (optional) | <scene-id>-template | <template id="act0-intro-bell-template"> |
Sub-comp root data-composition-id | <scene-id> (must match host slot) | data-composition-id="act0-intro-bell" |
| Timeline registry key | matches data-composition-id | window.__timelines["act0-intro-bell"] |
Host slot id | el-<short> or <scene-id> | id="el-intro", id="act0" |
| Element ids inside a sub-comp | prefix with the scene id | #act0-bell, #b1-tape |
| Audio at root | data-track-index well above visual tracks | 10 while visuals use 1 |
The -template suffix on <template> is conventional but not required — the runtime extracts contents from whichever <template> is in <body>, regardless of id. The prefix on inner element ids is the only safeguard against id collisions when multiple sub-comps are mounted into the same host page at once.
Editing Existing Projects
Before adding or modifying scenes, identify which architecture is in use:
ls compositions/ 2>/dev/null && echo "modular" || echo "monolithic"- In a monolithic project, add new scenes as inline
<section class="clip">elements with a non-overlappingdata-startand a sensibledata-track-index, and extend the existing single timeline. - In a modular project, match the pattern: add a new file under
compositions/, add a slot inindex.html, keep the root timeline thin. Do not start inlining new scenes intoindex.htmlwhen sibling scenes are sub-comps — the inconsistency is the worst of both worlds. - If a monolithic project needs a third or fourth scene cut, lift each scene into a sub-comp before adding more. The conversion is mechanical (see "Refactor between them" above).
When picking the slot's data-start/data-duration, prefer continuing the existing sequencing convention (adjoining starts, deliberate overlaps for cross-fades). Don't introduce a new track index unless you actually need parallel visual layers — most sequential-scene projects use exactly one visual track.
Data Attributes Reference
Every HyperFrames composition uses data-* attributes to declare timing and structure to the framework. This is the full attribute table — pair with tracks-and-clips.md for the rules behind data-track-index.
Composition Root
Every renderable composition needs one root element:
| Attribute | Required | Meaning |
|---|---|---|
data-composition-id | Yes | Unique ID. Must match the animation registry key on window.__timelines. |
data-width / data-height | Yes | Pixel frame size. Common values: 1920x1080, 1080x1920, 1080x1080. |
data-duration | Yes | Duration in seconds. This is the render duration, not the GSAP timeline length. |
data-fps | No | Optional frame rate hint. CLI render flags can override output fps. |
data-composition-variables | No | JSON array of variable declarations (on <html>). See variables-and-media.md. |
The root should be position: relative, have explicit pixel dimensions, and hide overflow unless intentionally composing outside the frame.
Clip Attributes
Timed child elements are clips. `class="clip"` is required on visible timed elements (<div>, <img>, etc.) — without it the runtime keeps the element visible for the whole composition, ignoring data-start / data-duration. Omit on <video> (framework manages visibility directly) and <audio> (no visual).
Clips must be DIRECT children of the composition root. A clip nested inside a wrapper <div> is not registered — most visibly, a <video> in a wrapper is never seeked/decoded and renders black. To wrap/transform a clip, put the wrapper _inside_ the clip, or animate the clip element itself; do not wrap the clip. (<video>/<audio> additionally must be at the host root, never in a sub-comp <template> — see variables-and-media.md.)
| Attribute | Required | Meaning |
|---|---|---|
id | Yes | Stable DOM ID for linting, timeline targets, and debugging. |
data-start | Yes | Start time in seconds, or a supported clip-time reference. |
data-duration | Required for div, img, and sub-compositions | Duration in seconds. Video/audio can default to media duration when known. |
data-track-index | Yes | Timeline track. Clips on the same track must not overlap. |
data-media-start | No | Offset into the media source, in seconds. |
data-volume | No | Static audio volume, 0 to 1, default 1. For fades, animate volume on the timeline instead (see variables-and-media.md). |
data-has-audio | No (<video> only) | "true" to declare the video carries an audio track when auto-detection would miss it. |
Visibility window is inclusive of both ends. A clip shows while start ≤ t ≤ start + duration — it still renders at exactly t = start + duration, so the final frame holds the animation's resolved end state (the runtime does not hide it one frame early). A reveal/entrance that lands on data-duration is therefore visible on the last frame; you do not need to finish it _before_ data-duration just to guarantee the end state renders. (Climax-dwell guidance in /hyperframes-animation is about pacing, not this boundary.)
Sub-Composition Host Attributes
When a clip is a sub-composition host (loads another composition file):
| Attribute | Required | Meaning |
|---|---|---|
data-composition-id | Yes | The internal composition ID of the loaded file. |
data-composition-src | Yes | Path to the sub-composition HTML file. |
data-width / data-height | Yes | Render dimensions for the sub-composition instance. |
data-variable-values | No | Per-instance variable overrides as JSON. See variables-and-media.md. |
See sub-compositions.md for the full wiring pattern.
Authoring Hints
id="root"— template convention used by scaffolds and the transition catalog so CSS can target the composition root with#rootinstead of[data-composition-id="main"]. Not required by the runtime, but consistent with the rest of the ecosystem.class="clip"— required runtime visibility marker on visible timed elements (<div>,<img>, …). See Clip Attributes above.data-layout-allow-overflow— tellshyperframes inspectthat overflow on this element (or its descendants) is intentional. Notes:inspectmeasuresgetBoundingClientRectat sampled timestamps, not rendered pixels —overflow: hiddenclips the visual but does not suppress aninspectoverflow finding. This attribute is the escape hatch; CSS overflow is not.- Can be set on the composition root as well as on any child. When the cited offender is
div.<comp>-root inside div.<comp>-root(the root reports its own children's union as overflowing), the fix goes on the root, not on individual text descendants — shrinking font sizes will not converge. - In a multi-scene
group_wN.html(continue runs), every scene-local element stays in the DOM during the other scenes' time windows; the layout-box union almost always overflows the canvas during morph seams. Mark the root and every scene-local primary/supporting element with this attribute at construction, not afterinspectflags it. - Blast radius — it silences more than `inspect`. The attribute is inherited down the subtree (the perception probe walks ancestors), so it also suppresses the rendered-perception checks
text-clipping,content-cramped-container, andforeground-over-panelfor every descendant. Putting it on a persistent panel that also hosts real foreground content disables collision checks on that content for the panel's whole lifetime. Prefer the narrowest opt-out: scope it to the smallest decorative wrapper, or use per-elementdata-layout-bleed="true"for one intentional primary-text crop. The two canvas/edge checksprimary-offscreenandforeground-over-paneldeliberately run even under allow-overflow, so it cannot hide a wordmark sliced by the frame or text bleeding onto a panel edge. data-layout-ignore— exclude this element from layout audits entirely.
Legacy / Removed Attributes
These names appear in older projects and examples. Use the current names when authoring or editing:
| Legacy name | Use instead |
|---|---|
data-layer | data-track-index |
data-end | data-duration |
Determinism, Animation Runtime, and Layout
HyperFrames seeks compositions frame-by-frame. Every frame must be reproducible from its time value alone — same input time → same pixels. Three contracts enforce this: the animation runtime contract, the determinism rules, and the layout contract.
Animation Runtime Contract
GSAP is the primary runtime. The core requirement is generic: animation state must be seekable from HyperFrames time.
For GSAP:
- Create the timeline synchronously during page initialization.
- Use
gsap.timeline({ paused: true }). - Register it on
window.__timelines["<composition-id>"]. - The key must match
data-composition-idon the composition root. - Do not call
tl.play()for render-critical motion. - Do not build timelines inside
async,Promise,setTimeout, or event handlers — the renderer can sample before they finish. - Do not create empty tweens only to set duration; use
data-durationon the clip instead. - Do not
gsap.set()clip elements from later scenes — they are not in the DOM at page load. Usetl.set(selector, vars, time)inside the timeline at or after the clip'sdata-start.
Use the hyperframes-animation skill for tween syntax, position parameters, eases, and performance rules.
Determinism Rules
Rendered frames must be reproducible from the requested time. Do not use any of the following for visual state:
Date.now(),performance.now(), or any render-time clock.- Unseeded
Math.random(). Use a seeded PRNG if random-looking placement is needed. - Render-time network fetches for required assets. Inline or pre-bundle them.
- Hover, scroll, pointer, or focus state. The renderer has no input events.
- Infinite loops such as
repeat: -1. Compute a finite repeat count from the visible duration:repeat: Math.ceil(duration / cycleDuration) - 1.
Also avoid:
- Animating anything outside the visual-property allowlist:
opacity,x,y,scale,rotation,color,backgroundColor,borderRadius, transforms. Never animatedisplayorvisibility— use opacity/transforms and timed clip visibility instead. - Animating the same property on the same element from multiple timelines at the same time — GSAP's overwrite behavior is order-dependent and can flip between renders.
Layout Contract
Build the visible end-state in static HTML and CSS first, then animate from/to that state.
- The composition root has fixed pixel frame dimensions.
- Scene containers should fill the scene with
width: 100%; height: 100%; box-sizing: border-box. - Use padding, flex, grid, and
max-widthfor layout. Avoid positioning main content with hardcodedtop/leftoffsets when a layout container can do it. - Use
position: absolutefor layers and decorative elements, not as the default content-layout strategy. - Prefer transforms and opacity for animation.
- Keep text inside its intended container. For dynamic text, use
max-width, wrapping, orwindow.__hyperframes.fitTextFontSize(text, { maxWidth, fontFamily, fontWeight }). - For text measurement without DOM reflow, use
window.__hyperframes.pretext:pretext.prepare(text, font)thenpretext.layout(prepared, maxWidth, lineHeight). Pure arithmetic, ~0.0002 ms per call — safe for per-frame text reflow, shrinkwrap containers, and computing layout before render.fitTextFontSizeis built on it. - Do not use
<br>in body text. Forced breaks ignore the actual rendered font width and produce an extra break when the line already wraps naturally, causing overlap. Let text wrap viamax-width. Exception: short display titles where each word is deliberately on its own line.
Why This Matters
The renderer takes a time value and produces a pixel buffer. There is no notion of "playback" — every frame is a fresh seek. Any state that depends on having reached this frame _through_ a prior frame (timers, accumulated state, event-driven animations) will desync when the renderer samples out of order or in parallel.
If you find yourself reaching for setTimeout, requestAnimationFrame, or addEventListener to drive a visual, rebuild it as a tween on the timeline instead.
Full-Screen Motion Pattern
For full-frame motion (continuous backgrounds, color washes, full-bleed visual states that span multiple clips), prefer a shared background layer + transparent timed content layers over stacked opaque scene backgrounds.
Why
Stacking opaque scene divs means every scene change has to repaint the entire frame, every cross-scene visual continuity has to be faked, and every "global" state (a hue shift, a vignette, a film grain) has to be duplicated on every scene. A shared background layer driven by the seekable timeline gives you one continuous visual surface and makes scenes themselves cheap and transparent.
Pattern
<div id="root" data-composition-id="main" data-width="1920" data-height="1080" data-duration="20">
<!-- Shared background — NOT a clip. Always visible. Driven by the timeline. -->
<div id="bg" class="full-bleed"></div>
<!-- Timed content layers — transparent backgrounds. -->
<section
id="scene1"
class="clip transparent"
data-start="0"
data-duration="6"
data-track-index="1"
>
<!-- content -->
</section>
<section
id="scene2"
class="clip transparent"
data-start="6"
data-duration="14"
data-track-index="1"
>
<!-- content -->
</section>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// Drive the shared background from the seekable timeline.
tl.to("#bg", { backgroundColor: "#0a1530", duration: 6, ease: "sine.inOut" }, 0);
tl.to("#bg", { backgroundColor: "#1a0a30", duration: 14, ease: "sine.inOut" }, 6);
// Scene-local animations stay transparent on top.
tl.from("#scene1 h1", { y: 48, opacity: 0, duration: 0.6 }, 0.2);
window.__timelines["main"] = tl;
</script>Rules
- The background is not a clip. No
data-start/data-duration/data-track-index. It exists for the whole composition. - Content scenes have transparent backgrounds. Whatever you put in the shared
#bgshows through. - Drive global state from the shared layer. Hue shifts, vignettes, grain, film-look filters — animate them once on the shared layer, not per-scene.
- Do not animate visibility on `.clip` elements. HyperFrames already shows/hides clips based on
data-startanddata-duration. Animatingdisplay/visibilityon the clip itself races with the framework's own show/hide. Animate a _child wrapper_ inside the clip instead. - Verify intentional overflow with snapshots. Before adding
data-layout-allow-overflowto silence an inspect warning, runnpx hyperframes snapshotand confirm the overflow is what you want.
When Not to Use This Pattern
If scenes really are visually disjoint — hard cuts between distinct color worlds with no continuity — the stacked-opaque pattern is fine. The shared-background pattern is for compositions where the background is part of the motion language, not just backdrop.
Minimal Composition
The smallest renderable HyperFrames composition — a standalone (top-level) root with one clip and one tween:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Minimal HyperFrames Composition</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
body {
margin: 0;
background: #0b0f14;
color: white;
font-family: Inter, system-ui, sans-serif;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
}
.clip {
position: absolute;
inset: 0;
display: grid;
place-items: center;
}
h1 {
margin: 0;
font-size: 96px;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-width="1920"
data-height="1080"
data-duration="5"
>
<section id="title-card" class="clip" data-start="0" data-duration="5" data-track-index="1">
<h1 id="title">Hello HyperFrames</h1>
</section>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.from("#title", { y: 48, opacity: 0, duration: 0.6, ease: "power3.out" }, 0.2);
window.__timelines["main"] = tl;
</script>
</body>
</html>Required elements:
- Root
<div>withdata-composition-id,data-width,data-height,data-duration - At least one clip (any element with
data-start,data-duration,data-track-index) - GSAP timeline created paused, registered on
window.__timelines["<composition-id>"]
This pattern is standalone (top-level index.html) — no <template> wrapper around the root. For sub-compositions (files loaded by data-composition-src), see sub-compositions.md.
Sub-Compositions
A sub-composition is a separate HTML file embedded in a host composition. HyperFrames loads it, seeks it independently, and composites the result into the host at data-start.
Host Wiring
In the host composition, the sub-composition appears as a clip with data-composition-src:
<div
id="chart"
data-composition-id="data-chart"
data-composition-src="compositions/data-chart.html"
data-start="2"
data-duration="8"
data-track-index="2"
data-width="1920"
data-height="1080"
></div>data-composition-idon the host must match the internaldata-composition-idof the file atdata-composition-src.- The host clip needs its own
data-start,data-duration,data-track-index,data-width,data-height.
Sub-Composition File Structure
Mental model — what the runtime actually does
When a host loads a sub-composition via data-composition-src, the runtime:
1. fetches the HTML file. 2. Parses it with DOMParser. 3. Finds the `<template>` element and clones ONLY its contents into the host slot. 4. Everything outside the <template> (including the entire <head>) is discarded.
So <template> is not just a wrapper — it is the transport container. If a node needs to exist in the live render, it must be inside <template>. Full stop.
File shape
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<!-- head is metadata for the source file only; the runtime ignores it -->
</head>
<body>
<template>
<!-- EVERYTHING the runtime needs goes here: styles, markup, scripts -->
<style>
.chart-root {
position: absolute;
inset: 0;
color: #fff;
}
/* ... */
</style>
<div class="chart-root" data-composition-id="data-chart" data-width="1920" data-height="1080">
<!-- sub-composition markup -->
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// ... build timeline ...
window.__timelines["data-chart"] = tl;
</script>
</template>
</body>
</html>Contrast with standalone compositions, which put the root directly in <body> with no <template> wrapper.
Common pitfalls that pass static checks but break at render
lint, validate, and inspect evaluate each file in isolation. These two failures live in the cross-file mount contract and cannot be caught until the runtime actually tries to mount the sub-composition. Watch for them at author time and verify with the pre-render checklist below.
Pitfall 1 — <style> in <head> instead of inside <template>
<!-- ❌ WRONG — looks normal, ships catastrophically broken -->
<head>
<style>
.chart-root { font-size: 88px; ... }
</style>
</head>
<body>
<template>
<div class="chart-root" ...>...</div>
</template>
</body>
<!-- ✅ RIGHT — styles are inside the template -->
<head></head>
<body>
<template>
<style>
.chart-root { font-size: 88px; ... }
</style>
<div class="chart-root" ...>...</div>
</template>
</body>Why this happens: standard HTML conventions tell you to put <style> in <head>. In a standalone HTML file that's correct. In a HyperFrames sub-composition it is not — the runtime only clones <template> contents, so <head><style> is dropped on the floor.
Symptom: the composition lints / validates / inspects clean, the render completes, but every text element shows up as tiny unstyled default text in the top-left and SVGs blow up to canvas-size because none of the CSS reached the live DOM. The same trap applies to <script> blocks, <link rel="stylesheet">, custom-element registrations — anything that needs to execute or apply in the render must be inside <template>.
Pitfall 2 — Host data-composition-id ≠ inner template data-composition-id
<!-- ❌ WRONG — host renames the slot; runtime can't find the timeline -->
<!-- host file (e.g. index.html) -->
<div data-composition-id="chart-mount" data-composition-src="compositions/chart.html" ...></div>
<!-- chart.html -->
<template>
<div data-composition-id="data-chart" ...>...</div>
<script>
window.__timelines["data-chart"] = tl;
</script>
</template>
<!-- ✅ RIGHT — both ids match, and the timeline key matches them too -->
<div data-composition-id="data-chart" data-composition-src="compositions/chart.html" ...></div>
<!-- chart.html template root: data-composition-id="data-chart" -->
<!-- timeline: window.__timelines["data-chart"] = tl; -->Why this happens: it feels natural to give the host slot a different name like chart-mount ("the mount point") vs data-chart ("the actual chart"). HyperFrames does not work that way — the host's `data-composition-id` is the lookup key the framework uses to find the registered timeline. Lint passes because each file's ids are individually valid; the cross-file mismatch only blows up at render.
Symptom: the render logs Sub-composition timelines not registered after 45000ms: <host-id> for every mismatched slot, waits 45s per scene, then captures static initial-state frames (so the video is full-length but no animation plays).
Verification checklist before render
# For every sub-composition file in compositions/:
# 1) <style> + <script> + main markup all live INSIDE <template>
grep -n "<style\|<script\|<template" compositions/<scene>.html
# → first <style>/<script>/<div data-composition-id> should be AFTER <template>, before </template>
# 2) host data-composition-id == internal data-composition-id == window.__timelines key
grep "data-composition-id" index.html
grep "data-composition-id\|__timelines\[" compositions/<scene>.html
# → all three strings must match exactly per sceneFor the runtime end-to-end check (a fast snapshot pass + per-scene frame eyeball), see the Visual smoke test step in hyperframes-cli's Minimum Completion Gate — that is the only gate that catches these two pitfalls.
What HyperFrames Does With the Sub-Composition
- Loads the file and registers its timeline under its internal
data-composition-id. - Seeks the sub-composition's timeline independently from the host's playhead.
- Plays the sub-composition's content from
data-startof the host clip, fordata-durationseconds.
Do not manually master.add(child) a sub-composition timeline into the host timeline. HyperFrames already drives them independently — nesting them in GSAP causes double-seeks.
Animations Inside Sub-Compositions
Prefer gsap.fromTo() over gsap.from() for entrance tweens. The host re-seeks the sub-composition every time its clip becomes visible; gsap.from() records the starting state at registration and can desync on seek-back, while gsap.fromTo() declares both endpoints explicitly and replays cleanly.
Per-Instance Variables
If the sub-composition declares variables on its <html> element (data-composition-variables), the host can override values per instance:
<div
data-composition-id="data-chart"
data-composition-src="compositions/data-chart.html"
data-variable-values='{"title":"Q4 Revenue","accent":"#66d9ef"}'
data-start="2"
data-duration="8"
data-track-index="2"
data-width="1920"
data-height="1080"
></div>The host can render the same sub-composition multiple times with different data-variable-values to produce per-instance variations. See variables-and-media.md for variable declaration syntax.
Subagent dispatch — harness adapter
The video workflows (product-launch-video / faceless-explainer / pr-to-video / motion-graphics / general-video) describe subagent dispatch in harness-neutral verbs. This file maps those verbs to the primitives of whatever agent harness you are running on. Read it once per run, before the first dispatch; everything else in the workflows (dispatch packets, file artifacts, exit-code gates, Resume tables) is harness-independent and needs no translation.
The contract (identical on every harness)
- DISPATCH(role_file, dispatch_context) — start one child agent whose prompt is the full contents of the named `agents/<role>.md` file followed by the
## Dispatch contextblock from the workflow, copied verbatim (never digested or paraphrased). Every harness below accepts arbitrary task text, so this works everywhere; never rely on the child seeing your conversation, memory, or skills — the prompt and the files on disk are its entire world. - Parallel fan-out — when a step says "start N workers in parallel", the workers are mutually independent (no ordering, no shared state beyond the filesystem). Run as many concurrently as your harness allows.
- WAIT — a step's completion criterion is always the expected artifact existing on disk (e.g.
compositions/<scene-id>.html), never the harness's completion notification (some harnesses deliver results best-effort). After waiting, verify the artifacts; a missing artifact means that child failed — re-dispatch it once with the same prompt before surfacing an error.
Concurrency cap → batching rule (cap never changes scope)
A harness concurrency limit reduces parallelism, not work: every scene still gets built, one scene per dispatch, with the available slots chewing through the full list.
- Harness queues excess children internally (Claude Code; Hermes
delegate_taskbeyondmax_concurrent_children) → submit all N at once and let the queue drain. - Harness hard-caps active children (e.g. OpenClaw
maxChildrenPerAgent) → dispatch in waves of the cap size: startcapworkers, wait for their artifacts, start the next wave, until all N scenes exist. Example: 9 scenes on a cap-3 harness = 3 waves of 3 — never drop scenes, never merge scenes into one worker to fit the cap.
Primitive map
| Verb in the workflows | Claude Code | Codex CLI | OpenClaw | Hermes Agent |
|---|---|---|---|---|
| DISPATCH | Agent tool, subagent_type: "general-purpose" | spawn_agent (built-in worker role; prompt carries the role file) | sessions_spawn (role file rides in task; child only auto-loads AGENTS.md/TOOLS.md) | delegate_task — put role file + context into each task's context (child system prompt is not customizable) |
| parallel fan-out | multiple Agent calls in one message, each run_in_background: true | multiple spawn_agent calls (async by design) or spawn_agents_on_csv for uniform batches | repeated sessions_spawn (always non-blocking) | one delegate_task(tasks=[...]) batch call |
| WAIT | background-task completion notifications arrive automatically | wait_agent with all child IDs | sessions_yield (never poll session lists) | delegate_task returns when all tasks finish (blocking the turn is normal) |
| default concurrency | min(16, cores − 2), excess queues | 6 (agents.max_threads) | 5 active per session, 8 global | 3 (delegation.max_concurrent_children) |
| re-dispatch after a gate failure | new Agent call, same prompt + the error/## Repair context | new spawn_agent, same prompt + the error | new sessions_spawn, same task + the error | new delegate_task, same context + the error |
Harness-specific notes:
- Codex: children inherit the parent cwd and cannot change it — exactly what these pipelines need (shared
PROJECT_DIR). Prefer prompt-carried roles over.codex/agents/*.tomlregistration; the workflows already deliver roles in the prompt. Codex policy-gates spawning on the user explicitly requesting delegation — a skill instruction alone does not satisfy it, and you will be tempted to skip the tool without even trying. Do not silently degrade: on Codex, if the user has not granted delegation this session, ask one line — "OK to run this workflow's workers as parallel subagents?" — folded into the workflow's first existing user pause (e.g. the API-key question) so the run pauses only once; at the latest, before the first dispatch step. An affirmative — or a standing grant in the workspaceAGENTS.mdor the kickoff prompt — unlocks native dispatch for the whole session. Only without a grant: prefer ladder rung 1 below (headlesscodex execchildren) for the scene fan-out; inline serial is the last resort. - OpenClaw: result announcement is best-effort (lost on gateway restart) — the artifact-on-disk WAIT rule above is the source of truth, not the announce message.
- Hermes: children get a fresh conversation and only see what you put in
context, and only the final summary returns — both already match the workflows' design (dispatch packets carry everything; results land on disk). Requires a local execution backend (the pipelines exchange data throughPROJECT_DIRand packet files on the shared filesystem; Docker/remote backends break that). - Any other harness, or subagents unavailable/disabled — degrade down this ladder:
1. Headless CLI children: write each child prompt to a file, start the CLI per worker as a background shell process (redirect stdin from /dev/null, output to a log), then collect artifacts from disk. 2. Inline serial execution: do each role yourself, one at a time — read the role file, follow it with the dispatch context as if you were the child, finish, then return to the runbook. Load only one role's resources at a time (a scene's role file + its packet), and still run every per-role self-check before moving on.
Vocabulary mapping (older phrasing you may meet in agent prompts)
- "in the same message" /
run_in_background: true— Claude Code idioms for "concurrently / as a background child"; apply your harness's equivalent from the table. - "Skill
X" / "loaded with the Skill tool" — load skill X; on harnesses without a skill-loading tool, read<skills-root>/X/SKILL.mddirectly (the skills root is the directory containing this skill family; derive it fromSKILL_DIRin your dispatch context). Read/Write/Edit/Bash— capability names (read file / write file / edit in place / run shell); map to your harness's tools.
HyperFrames Tailwind
HyperFrames init --tailwind uses the Tailwind browser runtime pinned by the scaffold. Treat it as Tailwind v4, not Studio's Tailwind v3 setup.
When To Use
- The project was scaffolded with
npx hyperframes init --tailwind. index.htmlcontainswindow.__tailwindReady.- The task asks for Tailwind utility classes,
@theme, custom utilities, or v3-to-v4 fixes in a composition. - Rendered frames have missing Tailwind styles or frame-0 flashes.
Version Contract
- Pinned: `@tailwindcss/browser@4.2.4` (source of truth:
packages/cli/src/commands/init.tsTAILWIND_BROWSER_VERSION). - Do not replace the scaffolded runtime with
cdn.tailwindcss.com(unpinned, defeats reproducibility). - Keep the readiness shim deterministic; HyperFrames waits for
window.__tailwindReadybefore frame 0 capture. - For offline / locked-down / production-stable renders, compile Tailwind to CSS and ship the stylesheet instead of the browser runtime.
v4 Browser Runtime Rules
Tailwind v4 is CSS-first:
<style type="text/tailwindcss">
@theme {
--color-brand: oklch(0.68 0.2 252);
--font-display: "Inter", sans-serif;
}
@utility headline-balance {
text-wrap: balance;
letter-spacing: 0;
}
</style>Avoid v3-only patterns in browser-runtime compositions:
@tailwind base;
@tailwind components;
@tailwind utilities;Do not add tailwind.config.js only for composition colors, fonts, spacing, or utilities. Use @theme and @utility.
Migrating from v3? Load an existing JS config explicitly: put @config "./tailwind.config.js"; inside a text/tailwindcss block. v4 does not auto-detect v3 config files.
Composition Pattern
Use Tailwind for static layout and style. Keep render-critical timing in GSAP or another seekable HyperFrames adapter.
<section
id="hero"
class="clip absolute inset-0 grid place-items-center bg-zinc-950 text-white"
data-start="0"
data-duration="5"
data-track-index="1"
>
<div class="w-[1280px] max-w-[82vw] text-center">
<h1 class="text-7xl font-black leading-none text-balance">Render-ready Tailwind</h1>
</div>
</section>For repeated items, parameterize via CSS variables — keep the class list static so the runtime sees every utility:
<span class="translate-y-[calc(var(--i)*6px)] opacity-80" style="--i: 0"></span>
<span class="translate-y-[calc(var(--i)*6px)] opacity-80" style="--i: 1"></span>
<span class="translate-y-[calc(var(--i)*6px)] opacity-80" style="--i: 2"></span>Dynamic Class Safety
The browser runtime scans classes it can see. Do not build render-critical class names only at seek time:
// Risky: the runtime may never see every generated class.
element.className = `bg-${color}-500`;Prefer complete class tokens in HTML, data variants, or explicit CSS:
<div data-tone="blue" class="bg-blue-500 data-[tone=rose]:bg-rose-500"></div>If a generated class is unavoidable, make sure the full class token appears in a text/tailwindcss block before validation.
Video-Specific Guardrails
v4 + render-mode footguns. Every bullet is a hard rule:
- Stable dimensions only — use
w-[…]/h-[…]/aspect-video/ grid / flex. No `md:` / `lg:` breakpoints (renderer is fixed-viewport). - Animate via transforms / opacity —
translate-*,scale-*,opacity-*are seek-safe; animating Tailwind sizing utilities is not. - *No `transition-` for render-critical motion** — a seekable runtime (GSAP) must own the state.
- No interaction variants —
hover:/focus:/active:/group-*:/peer-*:/ scroll / pointer variants never fire during render. - Bare `border` is broken in v4 — v4 default is
currentColor(v3 wasgray-200). Always write the color:border border-white/20. - v4 utility renames —
shadow-sm→shadow-xs,rounded-sm→rounded-xs,outline-none→outline-hidden,flex-shrink-*→shrink-*,flex-grow-*→grow-*. - Modern CSS is fine —
color-mix(), container queries, logical properties work; the renderer is current Chrome.
Validation
npx hyperframes lint
npx hyperframes validate
npx hyperframes inspect
# Render proof — frame 0 must NOT flash unstyled content. Preview alone can hide this.
npx hyperframes render . --workers 1 --quality draft --output tailwind-proof.mp4Quick Debug Checklist
When Tailwind styles don't apply in a render, check in order:
1. Project scaffolded with npx hyperframes init --tailwind? 2. index.html <head> has <script src="…@tailwindcss/browser@4.2.4…"> (not cdn.tailwindcss.com)? 3. window.__tailwindReady Promise present in <head>? 4. No v3 directives (@tailwind base/components/utilities) in the file? 5. Tokens moved from tailwind.config.js to @theme (or @config reference for v3 migration)? 6. Every render-critical class appears as a complete static token (no bg-${color}-500 style assembly)? 7. Re-run npx hyperframes validate, then the render proof above.
Tracks and Clips
Clips are timed children of the composition root. Tracks are a temporal-overlap concept, not a visual-stacking concept.
What is a Clip
A clip is any DOM element with data-start, data-duration (where required), and data-track-index. Common kinds:
- Visual `<div>` clips — scenes, cards, overlays. Always require
data-duration. - Sub-composition hosts —
<div>withdata-composition-src. Always requiredata-duration. - Video clips —
<video>withmutedandplaysinline. Duration can default to media length. - Audio clips —
<audio>. Duration can default to media length. - Image clips —
<img>. Always requiredata-duration.
Add class="clip" to authored visual clips so tooling and examples can find them.
Tracks Are Temporal, Not Visual
data-track-index controls temporal overlap, not paint order:
- Two clips on the same `data-track-index` must NOT overlap in time.
hyperframes lintflags this. - Visual layering (front/back) is controlled by CSS
z-index, not by track index.
A clip on track 5 is not "above" a clip on track 1 — it's just on a different audio/visual lane in time. Use CSS for layering, tracks for sequencing.
Picking a Track Index
There's no fixed convention, but common patterns:
- Track 0 — base video (e.g. an A-roll).
- Track 1+ — visual scenes, overlays, captions.
- Higher tracks (e.g. 10+) — audio clips, separated from visual tracks to keep linting clear.
When adding a new clip to an existing composition:
1. Find an existing track that has no overlap with your new clip's [data-start, data-start + data-duration) range. 2. Or pick a fresh track index. 3. Never overlap two clips on the same track — the linter will fail and the render is undefined.
Clip Time Inside the Composition
data-start is in seconds, measured from the start of the _composition_. For sub-compositions, the sub-composition's internal timeline (its own data-duration and child clips) runs from data-start to data-start + data-duration of the host.
data-media-start (on <video>/<audio>) is an offset _into the source media_. Use it to skip the first few seconds of a media file without trimming the file itself.
Relative Timing
data-start accepts a clip ID instead of a number, meaning "start when that clip ends". Add + N / - N to offset; negative produces overlap (useful for crossfades).
<video id="intro" data-start="0" data-duration="10" data-track-index="0" src="..."></video>
<video id="main" data-start="intro" data-duration="20" data-track-index="0" src="..."></video>
<video
id="scene-a"
data-start="intro + 2"
data-duration="20"
data-track-index="0"
src="..."
></video>
<video
id="scene-b"
data-start="intro - 0.5"
data-duration="20"
data-track-index="1"
src="..."
></video>Rules:
- References resolve inside the same composition only — cannot reach into a parent or sibling sub-composition.
- The referenced clip must have a known duration (explicit
data-durationor inferred from media). Otherwise the reference cannot resolve. - No circular references —
A → B → Ais rejected. Cycles are detected and error out. - A value that parses as a number is always treated as absolute seconds. Otherwise the resolver expects
<id>,<id> + <number>, or<id> - <number>(whitespace optional). - References can chain (
A → B → C). Keep chains under 3-4 levels for readability. - Negative offsets create overlap; overlapping clips must be on different tracks, same-track overlap is rejected.
Variables and Media
Two separate concerns, grouped because both control "what flows in from outside the HTML": runtime parameters (variables) and external media files (video/audio).
Variables
Declare variables on the <html> element with data-composition-variables. Each declaration needs id, type, label, and default:
<html
data-composition-variables='[
{"id":"title","type":"string","label":"Title","default":"Hello"},
{"id":"accent","type":"color","label":"Accent","default":"#66d9ef"}
]'
></html>Read resolved values once during initialization:
const { title, accent } = window.__hyperframes.getVariables();
document.getElementById("title").textContent = title;
document.documentElement.style.setProperty("--accent", accent);Variable Rules
- Supported types and their extra options (consumed by Studio's editing UI):
string— optionalplaceholder,maxLengthnumber— optionalmin,max,step,unitcolor— noneboolean— noneenum— requiredoptions: [{ "value": "...", "label": "..." }, ...]- Always provide useful
defaultvalues so preview works without CLI overrides. - Use
data-variable-values='{"title":"Pro"}'on sub-composition hosts for per-instance overrides. - Use
npx hyperframes render --variables '{"title":"Q4 Report"}'or--variables-filefor render-time overrides. - Add
--strict-variablesin CI: turns undeclared keys, type mismatches, and enum values not inoptionsinto errors instead of warnings. - Read values once during init, not on every animation tick — variables don't change mid-render.
Two JSON Shapes (Easy to Confuse)
data-composition-variablesis an array of declarations (the schema):[{id, type, label, default}, ...]--variablesanddata-variable-valuesare objects keyed by id (the values):{ title: "Q4", accent: "#fff" }
Media
NON-NEGOTIABLE: `<video>`/`<audio>` must be a DIRECT child of the host composition root (`index.html`). The runtime only registers + drives media that is a direct root child. Media placed inside a sub-composition <template>, or wrapped in any intermediate <div>, is never seeked/decoded → renders blank (paper/white) or black. lint/validate/inspect do not catch this; per-frame snapshot shows the blank panel.
Consequences:
- A scene-specific clip still lives at the host root, not in the scene's sub-comp. The sub-comp keeps only the frame/shell; the media is a sibling host element positioned over it.
- A sub-composition cannot reach or animate host elements — neither
document.querySelector("#host-id")nor a gsap selector string (tl.to("#host-id", …)) resolves across the boundary; a sub-comp timeline only drives its own subtree. So all per-scene motion on host media (scale/opacity/morph/tilt/breathing) must be authored on the MAIN timeline in `index.html`, at GLOBAL time (scene-local time + the scene slot'sdata-start). For 3D tilt without a perspective parent, use gsaptransformPerspectiveon the element. Seecomposition-patterns.mdarchetype B.
Video elements must be muted and inline. Audio must be a separate <audio> element, even when it uses the same source file.
<video
id="a-roll"
class="clip"
src="assets/demo.mp4"
data-start="0"
data-duration="12"
data-track-index="0"
muted
playsinline
></video>
<audio
id="a-roll-audio"
src="assets/demo.mp4"
data-start="0"
data-duration="12"
data-track-index="10"
data-volume="1"
></audio>Media Rules
- Do not call
video.play(),audio.play(), pause, or seek in composition code. HyperFrames owns playback. - Do not place media inside a sub-comp
<template>or any wrapper<div>— direct host-root child only (see above), else it never decodes. - Do not drive host media from a sub-comp timeline — it has no effect. Drive it from the main timeline at global time.
- Do not animate timed media element dimensions; animate a non-timed wrapper instead.
- Do not nest video inside a timed wrapper. Put timing on the media element or keep the wrapper untimed.
- Add
crossorigin="anonymous"for external media that needs canvas capture or pixel inspection. - Audio always lives on a separate
<audio>element — even if its source file is the same as a<video>. The<video>is muted; the<audio>carries sound. - For volume fades/ducking, animate
volumeon the timeline (tl.to("#bgm", { volume: 0, duration: 1 }, "outro")) rather than swappingdata-volume. The runtime probes the timeline's volume keyframes and applies them identically in preview and render;data-volumeis the static baseline for elements no tween touches.
For media duration: <video> and <audio> can omit data-duration if the media's intrinsic length is known and you want the full clip. Otherwise provide data-duration explicitly.
Related skills
How it compares
Start with hyperframes-core for structure and validation; add hyperframes-animation for motion and hyperframes-creative for brand direction.
FAQ
Why use HTML for video composition?
HTML is familiar to web developers, composable, and supports deterministic rendering - same markup produces identical frames.
What does deterministic mean?
Same HTML input always produces the exact same video frames, enabling CI/CD, regression testing, and reliable video generation.
Is Hyperframes Core safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.