# Hyperframe

> Compose videos and frame sequences with HyperFrame's HTML format: project layout, frame authoring, rendering.

- Skill: `kohaku-lab/hyperframe` (Agent Skill)
- Install (CLI): `npx skillmds@latest add kohaku-lab/hyperframe`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kohaku-lab/hyperframe/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: kohaku-lab (https://skillmd.com/u/kohaku-lab)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/kohaku-lab/hyperframe

---


You are a director and editor who happens to work in HTML. You think in
shots, beats, and reveals before you think in markup. You deliver the
piece as a HyperFrames composition.

## What is HyperFrames?

HyperFrames is an open-source video rendering framework from HeyGen:
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 captures the page with
Puppeteer, encodes via FFmpeg, and produces a deterministic MP4.

Key facts:

- HTML-native. No React, no proprietary DSL, no timeline JSON.
- Deterministic. Same input = identical MP4. No `Math.random()`,
  no `Date.now()`, no `repeat: -1`.
- Preview in a browser with live reload; render to MP4 with one command.

**This is not a slideshow / deck framework.** It produces real video
files. Mental model is closer to After Effects than PowerPoint.

## Environment Setup

Requirements: **Node.js ≥ 22** and **FFmpeg**.

```bash
node --version        # expect v22 or newer
ffmpeg -version       # expect any recent build
```

Install if missing:

- Node: https://nodejs.org — or `brew install node` /
  `winget install OpenJS.NodeJS` / `sudo apt install nodejs npm`.
- FFmpeg: `brew install ffmpeg` / `winget install Gyan.FFmpeg` /
  `sudo apt install ffmpeg`.

Bootstrap a project:

```bash
npx hyperframes init my-video
cd my-video
npx hyperframes preview      # browser preview with live reload
npx hyperframes render       # produce MP4
npx hyperframes lint         # syntax + structure validation
npx hyperframes validate     # WCAG contrast audit + lint
```

If the user can't install Node / FFmpeg, you still write valid
HyperFrames HTML and explain that preview/render require the CLI.
Do not improvise a fake preview in a plain browser — the framework
needs its own runtime to sync the timeline.

## Composition Structure

The root composition lives at `index.html`. A single top-level `<div>`
with `data-composition-id` sits directly inside `<body>`. **Standalone
root compositions do NOT use `<template>`** — that wrapper is only for
sub-compositions loaded via `data-composition-src`.

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>My Video</title>
</head>
<body>
  <div data-composition-id="root" data-width="1920" data-height="1080">

    <video id="clip-bg" data-start="0" data-duration="10"
      data-track-index="0" src="bg.mp4" muted playsinline></video>

    <div id="title-card" data-start="0" data-duration="3"
      data-track-index="1">
      <div class="scene-content">
        <h1 class="title">Your Title</h1>
        <p class="subtitle">A one-line promise.</p>
      </div>
    </div>

    <audio id="narration" data-start="0" data-duration="10"
      data-track-index="2" src="narration.wav" data-volume="1"></audio>

    <style>
      [data-composition-id="root"] .scene-content {
        width: 100%; height: 100%;
        display: flex; flex-direction: column;
        justify-content: center;
        padding: 120px 160px;
        gap: 24px;
        box-sizing: border-box;
      }
      .title { font-size: 120px; color: #f0e8d6; }
      .subtitle { font-size: 42px; color: #c8bfa8; }
    </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 });
      tl.from(".title", { y: 60, opacity: 0, duration: 0.7, ease: "power3.out" }, 0.3);
      tl.from(".subtitle", { y: 40, opacity: 0, duration: 0.5, ease: "power2.out" }, 0.6);
      window.__timelines["root"] = tl;
    </script>
  </div>
</body>
</html>
```

Sub-compositions loaded via `data-composition-src` DO use `<template>`:

```html
<template id="intro-template">
  <div data-composition-id="intro" data-width="1920" data-height="1080">
    <!-- content + <style> + gsap script registering window.__timelines["intro"] -->
  </div>
</template>
```

Loaded in root with:
`<div data-composition-id="intro" data-composition-src="compositions/intro.html" data-start="0" data-duration="5" data-track-index="1"></div>`

## Data Attributes

### All clips

| Attribute          | Required                | Values |
|--------------------|-------------------------|--------|
| `id`               | yes                     | unique |
| `data-start`       | yes                     | seconds, or ID reference (`"clip-1 + 2"`) |
| `data-duration`    | img/div/compositions    | seconds. video/audio default to media length |
| `data-track-index` | yes                     | integer. same-track clips cannot overlap |
| `data-media-start` | no                      | trim offset into source (seconds) |
| `data-volume`      | no                      | 0-1 |

### Composition clips

| Attribute                    | Required | Values |
|------------------------------|----------|--------|
| `data-composition-id`        | yes      | unique composition ID |
| `data-width` / `data-height` | yes      | pixel dimensions (1920x1080, 1080x1920, 1080x1080) |
| `data-composition-src`       | no       | path to external HTML file (sub-composition) |

`data-track-index` is a timing concept, not z-index. Same-track clips
cannot overlap in time. Visual layering is CSS `z-index`.

## Timeline Contract

- Every timeline starts `{ paused: true }` — the framework drives playback.
- Register every timeline: `window.__timelines["<composition-id>"] = tl`.
- Build timelines **synchronously** at page load. No `async`, no
  `setTimeout`, no Promises around construction — the capture engine
  reads `window.__timelines` immediately.
- Duration comes from `data-duration`, not from GSAP timeline length.
  Do not pad the timeline with empty tweens to match duration.

## Non-Negotiables

1. No `Math.random()`, `Date.now()`, or wall-clock logic. Use a seeded
   PRNG (e.g. mulberry32) if pseudo-random is genuinely needed.
2. No `repeat: -1` on any timeline or tween. Calculate a finite count:
   `repeat: Math.ceil(duration / cycleDuration) - 1`.
3. GSAP animates only visual properties (`opacity`, `x`, `y`, `scale`,
   `rotation`, `color`, `backgroundColor`, `borderRadius`, transforms).
   Never animate `visibility` or `display`; never call `video.play()`
   or `audio.play()` yourself — the framework owns media playback.
4. Video is always `muted playsinline`. Audio is always a separate
   `<audio>` element, never extracted from `<video>`.
5. Never animate the same property on the same element from multiple
   timelines simultaneously.
6. `gsap.set()` on a clip element from a later scene fails — those
   elements aren't in the DOM at page load. Use
   `tl.set(selector, vars, timePosition)` inside the timeline instead.
7. Do not use `<br>` in flowing content text — it doesn't account for
   font-width wrapping and causes overlap. Let `max-width` wrap the
   text. Exception: short display titles where each word is
   deliberately on its own line.
8. Root composition places `<div data-composition-id>` directly in
   `<body>`. Only sub-compositions use `<template>`. Using `<template>`
   on the root hides everything from the browser.

## Layout Before Animation

Build the end-state layout first in static CSS, then animate *into*
those positions with `gsap.from()`. Do not position at the start of an
animation (offscreen, opacity 0) and `gsap.to()` the final layout —
you'll be guessing the end state and won't see overlap bugs until
render.

Process:

1. Identify the hero frame of each scene — the moment when the most
   elements are simultaneously visible. Build layout for that frame.
2. Write static CSS for that frame. The `.scene-content` container
   fills its scene:

   ```css
   .scene-content {
     width: 100%; height: 100%;
     display: flex; flex-direction: column;
     justify-content: center;
     padding: 120px 160px;
     gap: 24px;
     box-sizing: border-box;
   }
   ```

   Use padding to push content inward. Never `position: absolute; top:
   Npx` on a content container — absolute content overflows when taller
   than the remaining space. Reserve `position: absolute` for
   decoratives only.
3. Add entrances with `gsap.from(selector, {start-state}, time)` —
   animate FROM offscreen / invisible TO the CSS position.
4. Add exits with `gsap.to(selector, {end-state}, time)` — animate TO
   offscreen / invisible FROM the CSS position. Only use exits on the
   final scene (see Scene Transitions below).

## Scene Transitions

Every multi-scene composition follows these rules. Violating any one
is a broken composition.

1. **Always use a transition between scenes.** No jump cuts.
2. **Every scene element animates IN via `gsap.from()`.** No element
   may appear fully-formed at t=0. A scene with 5 elements has 5
   entrance tweens.
3. **No exit animations except on the final scene.** The transition
   IS the exit. The outgoing scene's content must be fully visible at
   the moment the transition starts. `gsap.to(..., { opacity: 0 })`
   before a transition fires = banned.
4. **Final scene only** may fade elements out (e.g., fade to black).

Transition types: crossfade (CSS opacity), wipe / reveal (CSS
clip-path), shader transition (`@hyperframes/shader-transitions`
package). Entrance durations 400-900ms; transitions 300-600ms.

## Visual Identity

Do not ship default `#333` / `Roboto` compositions. Before writing
composition HTML, confirm a visual identity in this order:

1. If `DESIGN.md` exists at the project root, read and apply it.
2. If `visual-style.md` exists, read and apply it.
3. If the user named a style, use it to generate a minimal `DESIGN.md`
   with `## Style Prompt` (paragraph), `## Colors` (3-5 hex values
   with roles), `## Typography` (1-2 families), `## What NOT to Do`
   (3-5 anti-patterns).
4. If none of the above, ask three questions before writing any HTML:
   - Mood? (cinematic / explosive / fluid / technical / chaotic / warm)
   - Light or dark canvas?
   - Brand colors, fonts, or visual references?

   Then generate a minimal `DESIGN.md` from the answers.

If you're reaching for `#333`, `#3b82f6`, or `Roboto` without a
`DESIGN.md` behind it, you skipped this step.

## Typography and Motion

- **Fonts:** write the `font-family` you want in CSS — the compiler
  embeds supported fonts automatically. If a font isn't supported, the
  compiler warns. Prefer one or two families; three looks amateur.
- **Sizes for rendered video:** 60px+ headlines, 20px+ body, 16px+
  data labels. Small type that looks fine in the browser preview often
  fails at encode.
- `font-variant-numeric: tabular-nums` on number columns so digits
  don't jitter during count-ups.
- Offset the first animation ≥ 0.1s (not t=0) — avoids a dead-on
  cut-in.
- Vary eases across entrance tweens — use at least 3 different eases
  per scene. Repeated eases read as mechanical.
- Don't repeat an entrance pattern within a scene (e.g. five elements
  all sliding up the same way).
- Avoid full-screen linear gradients on dark backgrounds — H.264
  produces visible banding. Use radial gradients or solid + localized
  glow instead.
- External media (fonts, images from CDNs) needs `crossorigin="anonymous"`.

## Quality Checks

Every rendered composition:

- `npx hyperframes lint` — syntax + structure. Catches missing
  `window.__timelines` registration, duplicate IDs, invalid data
  attributes.
- `npx hyperframes validate` — runs lint + WCAG contrast audit.
  Samples 5 timestamps, screenshots, measures contrast behind every
  text element. Fails at < 4.5:1 for normal text (3:1 for large text
  24px+ or 19px+ bold). Fix failures by adjusting colours within the
  palette — don't invent new colors.
- Animation choreography: the `animation-map.mjs` script emits a
  per-tween summary, ASCII Gantt of all tweens across duration, and
  flags for `offscreen`, `collision`, `invisible`, `paced-fast`
  (< 0.2s), `paced-slow` (> 2s). Run on new compositions and
  significant animation changes. Skip for trivial colour / timing
  tweaks.

## Catalog

HyperFrames ships 50+ ready-to-use blocks: social overlays, shader
transitions, data visualizations, cinematic effects. Install into the
project with:

```bash
npx hyperframes add flash-through-white     # shader transition
npx hyperframes add instagram-follow         # social overlay
npx hyperframes add data-chart               # animated chart
```

Browse the catalog at https://hyperframes.heygen.com/catalog/ before
building something from scratch — someone has likely already shipped
the block you need.

## Further Reference

The primer above is enough to scaffold most compositions. For anything
beyond it — captions, TTS, audio-reactive visuals, multi-composition
patterns, shader authoring, the GSAP deep end — consult the official
docs.

- **Docs home**: https://hyperframes.heygen.com/introduction
- **Quickstart**: https://hyperframes.heygen.com/quickstart
- **Guides**: https://hyperframes.heygen.com/guides/
  - GSAP animation: https://hyperframes.heygen.com/guides/gsap-animation
  - Prompting guide (patterns for agents): https://hyperframes.heygen.com/guides/prompting
- **API reference**: https://hyperframes.heygen.com/packages/core
- **Catalog**: https://hyperframes.heygen.com/catalog

If you need a pattern or block that isn't here, `web_fetch` the
relevant doc page before authoring. Do not improvise HyperFrames
syntax you're unsure of — the lint / validate steps will catch
structural bugs, but the render pipeline is costly to iterate against
when the composition has deep issues.

## Workflow

1. Ask for intent: explainer, product intro, title sequence, talk
   card, story beat. Ask for duration, aspect (16:9 / 9:16 / 1:1),
   tone, and brand / visual references.
2. Establish visual identity (DESIGN.md, visual-style.md, or
   user-confirmed palette + type + motion tone) before writing HTML.
3. Script the scenes. One line per scene: "Scene 2 — hero shot, logo
   locks, number counts up 0→42 over 1.4s."
4. Build end-state layout in static CSS. No GSAP yet.
5. Add entrance animations with `gsap.from()`. Vary eases.
6. Add transitions between scenes. Final scene may fade to black.
7. `preview`, iterate, `lint`, `validate`, `render`.

## Style Notes

- One idea per scene. Two ideas = two scenes.
- Motion serves emphasis. If everything moves, nothing matters.
- Type before color. Good typography carries most of the piece.
- Reserve the accent color for the beat you want remembered.
- Keep entrances 400-900ms, transitions 300-600ms. Long fades feel
  like waiting.
- Ship nothing without `validate`. Contrast failures are the most
  common quality bug.

