HyperFrames

SkillMedia

With hyperframes, your AI can write HTML and render it into video. It is a skill built for agents, so both the writing and the rendering are meant to happen through your AI. Once added, your AI can go from HTML it creates to a finished video.

Use HyperFrames in Claude, ChatGPT or Ahel Desktop

Free. Sign in, add HyperFrames and connect your AI. About a minute.

Also: Claude Code · Cursor · Codex

Then ask your AI: use the HyperFrames skill

Details

Instructions available. Your AI can read the instructions. Execution depends on the setup they require.

Add the skill, then ask your AI to write some HTML and render it as a video. The source code is available at github.com/heygen-com/hyperframes.

HyperFramesStart free

What your AI can do with it

  • Write HTML
  • Render HTML into video
  • Turn HTML it writes into finished video output

What this skill tells your AI

The instructions your AI receives, as published by alecs5am/ralphy in .agents/skills/hyperframes/SKILL.md and read by Ahel’s review.

HTML is the source of truth for video. A composition is an HTML file with data-* attributes for timing, a GSAP timeline for animation, and CSS for appearance. The framework handles clip visibility, media playback, and timeline sync.

Approach

Discovery (exploratory requests only)

For open-ended requests ("make me a product launch video", "create something for our brand") where the user hasn't committed to a direction, understand intent before picking colors:

  • Audience — who watches this? Developers? Executives? General consumers?
  • Platform — where does it play? Social (15s), website hero, product demo, internal?
  • Priority — what matters most? Motion quality? Content accuracy? Brand fidelity? Speed?
  • Variations — does the user want options, or a single best shot?

For specific requests ("add a title card", "fix the timing on scene 3"), skip discovery.

For exploratory requests, consider offering 2-3 variations that differ meaningfully — not just color swaps, but different pacing, energy levels, or structural approaches. One safe/expected, one ambitious. Don't mandate this — it's a tool available when appropriate.

Step 1: Design system

If design.md or DESIGN.md exists in the project, read it first (check both casings — they're different files on Linux). It's the source of truth for brand colors, fonts, and constraints. Use its exact values — don't invent colors or substitute fonts. Any format works (YAML frontmatter, prose, tables — just extract the values).

If it names fonts you can't find locally (no fonts/ directory with .woff2 files, not a built-in font), warn the user before writing HTML: "design.md specifies [font name] but no font files found. Please add .woff2 files to fonts/ or I'll fall back to [closest built-in alternative]."

If no design.md exists, offer the user a choice:

  1. User named a style or mood? → Read visual-styles.md for the 8 named presets. Pick the closest match.
  2. Want to browse options visually? → Run the design picker: read references/design-picker.md for the full workflow. This serves a visual picker page. The user configures mood, palette, typography, and motion in the browser, then copies the generated design.md and pastes it back into the conversation.
  3. Want to skip and go fast? → Ask: mood, light or dark, any brand colors/fonts? Then pick a palette from house-style.md.

design.md defines the brand. It does not define video composition rules. Those come from references/video-composition.md and house-style.md. Use brand colors at video-appropriate scale — not at web-UI opacity.

Step 2: Prompt expansion

Always run on every composition (except single-scene pieces and trivial edits). This step grounds the user's intent against design.md and house-style.md and produces a consistent intermediate that every downstream agent reads the same way.

Read references/prompt-expansion.md for the full process and output format.

Step 3: Plan

Before writing HTML, think at a high level:

  1. What — what should the viewer experience? Identify the narrative arc, key moments, and emotional beats.
  2. Structure — how many compositions, which are sub-compositions vs inline, what tracks carry what (video, audio, overlays, captions).
  3. Rhythm — declare your scene rhythm before implementing. Which scenes are quick hits, which are holds, where do shaders land, where does energy peak. Name the pattern: fast-fast-SLOW-fast-SHADER-hold. Read references/beat-direction.md for rhythm templates.
  4. Timing — which clips drive the duration, where do transitions land, what's the pacing.
  5. Layout — build the end-state first. See "Layout Before Animation" below.
  6. Animate — then add motion using the rules below.

Build what was asked. A request for "a title card" is not a request for "a title card + 3 supporting scenes + ambient music + captions." Every scene, every element, every tween should earn its place. If additional scenes or elements would genuinely improve the piece, propose them — don't add them.

For small edits (fix a color, adjust timing, add one element), skip straight to the rules.

Layout Before Animation

Position every element where it should be at its most visible moment — the frame where it's fully entered, correctly placed, and not yet exiting. Write this as static HTML+CSS first. No GSAP yet.

Why this matters: If you position elements at their animated start state (offscreen, scaled to 0, opacity 0) and tween them to where you think they should land, you're guessing the final layout. Overlaps are invisible until the video renders. By building the end state first, you can see and fix layout problems before adding any motion.

The process

  1. Identify the hero frame for each scene — the moment when the most elements are simultaneously visible. This is the layout you build.
  2. Write static CSS for that frame. The .scene-content container MUST fill the full scene using width: 100%; height: 100%; padding: Npx; with display: flex; flex-direction: column; gap: Npx; box-sizing: border-box. Use padding to push content inward — NEVER position: absolute; top: Npx on a content container. Absolute-positioned content containers overflow when content is taller than the remaining space. Reserve position: absolute for decoratives only.
  3. Add entrances with gsap.from() — animate FROM offscreen/invisible TO the CSS position. The CSS position is the ground truth; the tween describes the journey to get there. (In sub-compositions loaded via data-composition-src, prefer gsap.fromTo() — see load-bearing GSAP rules in references/motion-principles.md.)
  4. Add exits with gsap.to() — animate TO offscreen/invisible FROM the CSS position.

Example

/* scene-content fills the scene, padding positions content */
.scene-content {
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 100%;
  height: 100%;
  padding: 120px 160px;
  gap: 24px;
  box-sizing: border-box;
}
.title {
  font-size: 120px;
}
.subtitle {
  font-size: 42px;
}
/* Container fills any scene size (1920x1080, 1080x1920, etc).
   Padding positions content. Flex + gap handles spacing. */

WRONG — hardcoded dimensions and absolute positioning:

.scene-content {
  position: absolute;
  top: 200px;
  left: 160px;
  width: 1920px;
  height: 1080px;
  display: flex; /* ... */
}
// Step 3: Animate INTO those positions
tl.from(".title", { y: 60, opacity: 0, duration: 0.6, ease: "power3.out" }, 0);
tl.from(".subtitle", { y: 40, opacity: 0, duration: 0.5, ease: "power3.out" }, 0.2);
tl.from(".logo", { scale: 0.8, opacity: 0, duration: 0.4, ease: "power2.out" }, 0.3);

// Step 4: Animate OUT from those positions
tl.to(".title", { y: -40, opacity: 0, duration: 0.4, ease: "power2.in" }, 3);
tl.to(".subtitle", { y: -30, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.1);
tl.to(".logo", { scale: 0.9, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.2);

When elements share space across time

If element A exits before element B enters in the same area, both should have correct CSS positions for their respective hero frames. The timeline ordering guarantees they never visually coexist — but if you skip the layout step, you won't catch the case where they accidentally overlap due to a timing error.

What counts as intentional overlap

Layered effects (glow behind text, shadow elements, background patterns) and z-stacked designs (card stacks, depth layers) are intentional. The layout step is about catching unintentional overlap — two headlines landing on top of each other, a stat covering a label, content bleeding off-frame.

Data Attributes

All Clips

AttributeRequiredValues
idYesUnique identifier
data-startYesSeconds or clip ID reference ("el-1", "intro + 2")
data-durationRequired for img/div/compositionsSeconds. Video/audio defaults to media duration.
data-track-indexYesInteger. Same-track clips cannot overlap.
data-media-startNoTrim offset into source (seconds)
data-volumeNo0-1 (default 1)

data-track-index does not affect visual layering — use CSS z-index.

Composition Clips

AttributeRequiredValues
data-composition-idYesUnique composition ID
data-startYesStart time (root composition: use "0")
data-durationYesTakes precedence over GSAP timeline duration
data-width / data-heightYesPixel dimensions (1920x1080 or 1080x1920)
data-composition-srcNoPath to external HTML file
data-variable-valuesNoJSON object of per-instance variable overrides on a sub-comp host

On the root <html> element:

AttributeRequiredValues
data-composition-variablesNoJSON array of declared variables (id/type/label/default) — drives Studio editing UI and provides defaults for getVariables()

Composition Structure

Sub-compositions loaded via data-composition-src use a <template> wrapper. Standalone compositions (the main index.html) do NOT use <template> — they put the data-composition-id div directly in <body>. Using <template> on a standalone file hides all content from the browser and breaks rendering.

Sub-composition structure:

<template id="my-comp-template">
  <div data-composition-id="my-comp" data-width="1920" data-height="1080">
    <!-- content -->
    <style>
      [data-composition-id="my-comp"] {
        /* scoped styles */
      }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
    <script>
      window.__timelines = window.__timelines || {};
      const tl = gsap.timeline({ paused: true });
      // tweens...
      window.__timelines["my-comp"] = tl;
    </script>
  </div>
</template>

Load in root: <div id="el-1" data-composition-id="my-comp" data-composition-src="compositions/my-comp.html" data-start="0" data-duration="10" data-track-index="1"></div>

Variables (Parametrized Compositions)

Render the same composition with different content — title, theme color, prices, captions — without editing the source HTML.

Three-step pattern:

  1. Declare variables on the composition's <html> root with data-composition-variables. Each entry needs id, type (one of string, number, color, boolean, enum), label, and default. Enum entries also need options: [{value, label}, ...].
  2. Read the resolved values inside the composition's script with window.__hyperframes.getVariables(). Returns the merged result of declared defaults + per-instance overrides + CLI overrides.
  3. Override at render time with npx hyperframes render --variables '{...}' (top-level) or with data-variable-values='{...}' on the host element (per-instance for sub-comps).
<!doctype html>
<html
  data-composition-variables='[
  {"id":"title","type":"string","label":"Title","default":"Hello"},
  {"id":"theme","type":"enum","label":"Theme","default":"light","options":[
    {"value":"light","label":"Light"},
    {"value":"dark","label":"Dark"}
  ]}
]'
>
  <body>
    <div data-composition-id="root" data-width="1920" data-height="1080">
      <h1 id="hero" class="clip" data-start="0" data-duration="3"></h1>
      <script>
        const { title, theme } = window.__hyperframes.getVariables();
        document.getElementById("hero").textContent = title;
        document.body.dataset.theme = theme;
      </script>
    </div>
  </body>
</html>
# Dev preview uses declared defaults
npx hyperframes preview

# Render with overrides
npx hyperframes render --variables '{"title":"Q4 Report","theme":"dark"}' --output q4.mp4

# Or from a JSON file
npx hyperframes render --variables-file ./vars.json

Sub-composition per-instance values: the same getVariables() works inside sub-comps loaded via data-composition-src. Each host element passes its own values:

<div
  data-composition-id="card-pro"
  data-composition-src="compositions/card.html"
  data-variable-values='{"title":"Pro","price":"$29"}'
></div>
<div
  data-composition-id="card-enterprise"
  data-composition-src="compositions/card.html"
  data-variable-values='{"title":"Enterprise","price":"Custom"}'
></div>

The runtime layers each host's data-variable-values over the sub-comp's declared defaults on a per-instance basis, so the same source can be embedded multiple times with different content.

Rules of thumb:

  • Always provide a sensible default for every declared variable. Dev preview uses defaults — without them, the composition won't render correctly until --variables is provided.
  • Read variables once at the top of the script (const { title } = ...), not inside frame loops or event handlers — getVariables() allocates a fresh object per call.
  • Use --strict-variables in CI to fail fast on undeclared keys or type mismatches.
  • Variable types are validated at render time. string, number, boolean, and color (hex string) check typeof; enum checks the value is in the declared options.

Video and Audio

Video must be muted playsinline. Audio is always a separate <audio> element:

<video
  id="el-v"
  data-start="0"
  data-duration="30"
  data-track-index="0"
  src="video.mp4"
  muted
  playsinline
></video>
<audio
  id="el-a"
  data-start="0"
  data-duration="30"
  data-track-index="2"
  src="video.mp4"
  data-volume="1"
></audio>

HARD RULES for timed media (#047)

Both rules below are enforced by cli/lib/render/hyperframes-lint.ts before every ralphy render call. Violating either causes a silent freeze at capture time, not a clean error — that is why they are pre-render gates.

  1. Timed media carries id + data-start on the element itself, never on a wrapper. A <div data-start="0" data-track-index="0"><video src="..."></video></div> wrapper is invisible to the HyperFrames media scheduler — the runtime reads timing from the <video> / <audio> element directly. The render-time lint codes media_missing_id and media_missing_data_start are now author- time errors that block render.

    <!-- WRONG — wrapper owns the timing attrs -->
    <div data-start="0" data-track-index="0" data-duration="6">
      <video src="clip.mp4" muted playsinline></video>
    </div>
    
    <!-- RIGHT — attrs on the element itself -->
    <video
      id="clip-1"
      data-start="0"
      data-track-index="0"
      data-duration="6"
      src="clip.mp4"
      muted
      playsinline
    ></video>
    
  2. For a montage of N short clips on one track, concat them into a single video. The HyperFrames runtime cannot reliably switch between many short same-track video clips during capture — typically only the first plays and the rest render as a static frame. When > 4 <video> elements share a data-track-index AND each data-duration is < 3 seconds, the pre-render lint warns with a concat-fix suggestion. Either:

    • Concat the clips with ffmpeg -f concat -i list.txt -c copy out.mp4 and reference the single resulting video, OR
    • Put each clip on its own data-track-index if they really must remain separate (e.g. for per-clip animation), OR
    • Add data-allow-short-stack="true" to any of the affected <video> tags once you have verified the composition renders correctly (override).

    Origin: ralphy-vs-higgsfield-001 postmortem — 6×2s clips on track 0 rendered only the first, others frozen / blank.

Timeline Contract

  • All timelines start { paused: true } — the player controls playback
  • Register every timeline: window.__timelines["<composition-id>"] = tl
  • Framework auto-nests sub-timelines — do NOT manually add them
  • Duration comes from data-duration, not from GSAP timeline length
  • Never create empty tweens to set duration

Rules (Non-Negotiable)

Deterministic: No Math.random(), Date.now(), or time-based logic. Use a seeded PRNG if you need pseudo-random values (e.g. mulberry32).

GSAP: Only animate visual properties (opacity, x, y, scale, rotation, color, backgroundColor, borderRadius, transforms). Do NOT animate visibility, display, or call video.play()/audio.play().

Animation conflicts: Never animate the same property on the same element from multiple timelines simultaneously.

No repeat: -1: Infinite-repeat timelines break the capture engine. Calculate the exact repeat count from composition duration: repeat: Math.ceil(duration / cycleDuration) - 1.

Synchronous timeline construction: Never build timelines inside async/await, setTimeout, or Promises. The capture engine reads window.__timelines synchronously after page load. Fonts are embedded by the compiler, so they're available immediately — no need to wait for font loading.

Never do:

  1. Forget window.__timelines registration
  2. Use video for audio — always muted video + separate <audio>
  3. Nest video inside a timed div — use a non-timed wrapper. The runtime reads id + data-start + data-track-index + data-duration from the <video> / <audio> element ITSELF, never from a parent. See "HARD RULES for timed media" below. Enforced by cli/lib/render/hyperframes-lint.ts (#047)
  4. Use data-layer (use data-track-index) or data-end (use data-duration)
  5. Animate video element dimensions — animate a wrapper div
  6. Call play/pause/seek on media — framework owns playback
  7. Create a top-level container without data-composition-id
  8. Use repeat: -1 on any timeline or tween — always finite repeats
  9. Build timelines asynchronously (inside async, setTimeout, Promise)
  10. Use gsap.set() on clip elements from later scenes — they don't exist in the DOM at page load. Use tl.set(selector, vars, timePosition) inside the timeline at or after the clip's data-start time instead.
  11. Use <br> in content text — forced line breaks don't account for actual rendered font width. Text that wraps naturally + a <br> produces an extra unwanted break, causing overlap. Let text wrap via max-width instead. Exception: short display titles where each word is deliberately on its own line (e.g., "THE\nIMMORTAL\nGAME" at 130px).

Scene Transitions (Non-Negotiable)

Every multi-scene composition MUST follow ALL of these rules. Violating any one of them is a broken composition.

  1. ALWAYS use transitions between scenes. No jump cuts. No exceptions.
  2. ALWAYS use entrance animations on every scene. Every element animates IN via gsap.from(). No element may appear fully-formed. If a scene has 5 elements, it needs 5 entrance tweens.
  3. NEVER use exit animations except on the final scene. This means: NO gsap.to() that animates opacity to 0, y offscreen, scale to 0, or any other "out" animation before a transition fires. The transition IS the exit. The outgoing scene's content MUST be fully visible at the moment the transition starts.
  4. Final scene only: The last scene may fade elements out (e.g., fade to black). This is the ONLY scene where gsap.to(..., { opacity: 0 }) is allowed.

WRONG — exit animation before transition:

// BANNED — this empties the scene before the transition can use it
tl.to("#s1-title", { opacity: 0, y: -40, duration: 0.4 }, 6.5);
tl.to("#s1-subtitle", { opacity: 0, duration: 0.3 }, 6.7);
// transition fires on empty frame

RIGHT — entrance only, transition handles exit:

// Scene 1 entrance animations
tl.from("#s1-title", { y: 50, opacity: 0, duration: 0.7, ease: "power3.out" }, 0.3);
tl.from("#s1-subtitle", { y: 30, opacity: 0, duration: 0.5, ease: "power2.out" }, 0.6);
// NO exit tweens — transition at 7.2s handles the scene change
// Scene 2 entrance animations
tl.from("#s2-heading", { x: -40, opacity: 0, duration: 0.6, ease: "expo.out" }, 8.0);

Animation Guardrails

  • Offset first animation 0.1-0.3s (not t=0)
  • Vary eases across entrance tweens — use at least 3 different eases per scene
  • Don't repeat an entrance pattern within a scene
  • Avoid full-screen linear gradients on dark backgrounds (H.264 banding — use radial or solid + localized glow)
  • 60px+ headlines, 20px+ body, 16px+ data labels for rendered video
  • font-variant-numeric: tabular-nums on number columns

If no design.md exists, follow house-style.md for aesthetic defaults.

Typography and Assets

  • Built-in fonts: Write the font-family you want in CSS — the compiler embeds supported fonts automatically.
  • Custom fonts: If design.md names a font that isn't built-in, the user must provide .woff2 files in a fonts/ directory. If missing, warn before writing HTML. When files exist, add @font-face declarations pointing to the local files.
  • Add crossorigin="anonymous" to external media
  • For dynamic text overflow, use window.__hyperframes.fitTextFontSize(text, { maxWidth, fontFamily, fontWeight })
  • All files live at the project root alongside index.html; sub-compositions use ../

Editing Existing Compositions

  • Read actual files, don't guess. When editing, extending, or creating companion compositions, read the existing source. Don't reconstruct hex codes from memory. Don't guess GSAP easing patterns. The composition IS the spec — extract exact values from it.
  • Match existing fonts, colors, animation patterns from what you read
  • Only change what was requested
  • Preserve timing of unrelated clips

Output Checklist

Fast (run immediately, block on results):

  • npx hyperframes lint and npx hyperframes validate both pass
  • Design adherence verified if design.md exists

Slow (run in parallel while presenting the preview to the user):

  • npx hyperframes inspect passes, or every reported overflow is intentionally marked
  • Contrast warnings addressed (see Quality Checks below)
  • Animation choreography verified (see Quality Checks below)

Quality Checks

Visual Inspect

hyperframes inspect runs the composition in headless Chrome, seeks through the timeline, and maps visual layout issues with timestamps, selectors, bounding boxes, and fix hints. Run it after lint and validate:

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
136
Forks
13
Last commit
Sep 2026
Hacker News mentions
6

What people say

Users report that HyperFrames works well with Claude Code/Codex to generate polished videos from HTML, with one-command setup and surprisingly good results. The recurring caveat is that output quality depends heavily on investing real effort in prompts and workflows (storyboards, frame.md).

Praised for
one-command setup with Claude Code
surprisingly polished video output
simple HTML-to-MP4, no React/Remotion complexity
Complaints
quality requires significant prompt/workflow effort

Summarized from 19 public posts. Opinions are theirs, not Ahel's.

Ahel review

  • K1binfo
    installs-packages (in scripts/package-loader.mjs)

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Item type
skill
Key
hyperframes
Source
github.com/alecs5am/ralphy