Files
hyperframes/docs/concepts/compositions.mdx
T
ukimsanov eb199bd159 docs: make the two hardest-reading pages readable, and draw the two core concepts
**Prompting.** These were the only two pages that stayed hard to read once the
sentence counter was corrected for bullet lists. Capstone went from 46% of
sentences over 28 words to 20%, and 53 semicolons to 42. Motion went from 37% to
15%, 26 semicolons to 4, and lost six table rows that were prose in a grid. Both
keep every technique and every worked example — the length barely moved, the
sentences did.

**Concepts.** Compositions and Determinism explain how the whole thing works and
showed the reader nothing. Each has one mermaid diagram now: how an HTML file
with data-* timing becomes a numbered sequence of frames, and why frame 90 always
comes out the same — what is locked before frame 0, and the three things that
break it.

Mermaid renders natively in Mintlify, so these cost no asset, no CDN upload and no
regeneration. Verified in the browser rather than assumed; the pattern is the one
proven on concepts/frame-adapters.
2026-08-04 03:08:04 -07:00

251 lines
8.5 KiB
Plaintext

---
title: Compositions
description: "The fundamental building block of a Hyperframes video."
---
A composition is an HTML file that describes a video. You write elements, give
them `data-*` timing attributes, and the framework turns them into frames.
`index.html` is the top-level composition. It can hold other compositions
inside it. There is no special "root" type — any composition can be imported
into any other.
## How an HTML file becomes frames
Here is a whole video: a logo, then a video clip.
```html index.html
<div id="root" data-composition-id="root"
data-start="0" data-width="1920" data-height="1080">
<img class="clip" src="assets/logo.png"
data-start="0" data-duration="2" data-track-index="0" />
<video class="clip" src="assets/video.mp4"
data-start="2" data-duration="3" data-track-index="0"></video>
</div>
```
Two rules make that work. The outer element needs `data-composition-id`, and
every timed element needs `class="clip"` so the runtime can hide it outside its
own window of time.
The clips add up to five seconds. At 30 frames per second — the default — that
is 150 frames, numbered 0 to 149:
```mermaid
flowchart TD
A["index.html<br/>img: start 0, duration 2<br/>video: start 2, duration 3"]
A --> B["One timeline, 5 seconds<br/>30 fps = 150 frames"]
B --> C["frames 0-59<br/>image on screen"]
B --> D["frames 60-149<br/>video on screen, playing<br/>from its own second 0"]
```
Nothing plays in real time during a render. The renderer asks for frame 0, then
frame 1, and each answer is a single still picture. That is what makes
[rendering deterministic](/concepts/determinism).
## What can go on the timeline
A clip is any element with timing attributes on it:
- `<video>` — video clips, B-roll, A-roll
- `<img>` — stills and overlays
- `<audio>` — music and sound effects
- `<div data-composition-id="...">` — another composition nested inside this one
The [Data attributes](/concepts/data-attributes) page lists every timing
attribute. The [HTML schema](/reference/html-schema) has the full contract.
## Put one composition inside another
You can either point at a separate file or write the nested composition inline.
Use a separate file when you want to reuse it.
<Tabs>
<Tab title="External file">
`data-composition-src` names the file. The framework fetches it, pulls the
content out of its `<template>` tag, mounts it, runs its scripts, and
registers its timeline.
Paths resolve from the **project root**, not from the file doing the
referencing. So a composition nested one level deep still writes
`compositions/foo.html`, never `../compositions/foo.html`.
```html index.html
<div
id="el-5"
data-composition-id="intro-anim"
data-composition-src="compositions/intro-anim.html"
data-start="0"
data-duration="4"
data-playback-start="0"
data-track-index="3"
></div>
```
The file it points at wraps everything in a `<template>`:
```html compositions/intro-anim.html
<template id="intro-anim-template">
<div data-composition-id="intro-anim" data-width="1920" data-height="1080">
<div class="title">Welcome!</div>
<style>
[data-composition-id="intro-anim"] .title {
font-size: 72px; color: white; text-align: center;
}
</style>
<script>
const tl = gsap.timeline({ paused: true });
tl.from(".title", { opacity: 0, y: -50, duration: 1 });
window.__timelines["intro-anim"] = tl;
</script>
</div>
</template>
```
`data-playback-start` picks which moment of the child timeline shows first.
It defaults to `0`. Trimming or splitting from the left pushes this offset
forward by the time elapsed multiplied by `data-playback-rate`, so the
nested animation keeps going instead of jumping back to its beginning.
</Tab>
<Tab title="Inline">
Write the nested composition straight into the parent. Simpler for
something you only use once.
```html index.html
<div id="root" data-composition-id="root"
data-start="0" data-width="1920" data-height="1080">
<!-- Inline nested composition -->
<div id="el-5" data-composition-id="intro-anim"
data-start="0" data-track-index="3"
data-width="1920" data-height="1080">
<div class="title">Welcome!</div>
</div>
<script>
// Timeline for the inline composition
const introTl = gsap.timeline({ paused: true });
introTl.from(".title", { opacity: 0, y: -50, duration: 1 });
window.__timelines["intro-anim"] = introTl;
</script>
</div>
```
No `<template>` tag and no `data-composition-src` here.
</Tab>
</Tabs>
### Where the files live
<Tree>
<Tree.Folder name="project" defaultOpen>
<Tree.File name="index.html" />
<Tree.Folder name="compositions" defaultOpen>
<Tree.File name="intro-anim.html" />
<Tree.File name="caption-overlay.html" />
<Tree.File name="outro-title.html" />
</Tree.Folder>
<Tree.Folder name="assets">
<Tree.File name="video.mp4" />
<Tree.File name="music.mp3" />
<Tree.File name="logo.png" />
</Tree.Folder>
</Tree.Folder>
</Tree>
## HTML sets the timing, scripts do the motion
Your HTML says what plays, when, and on which track, all through
[data attributes](/concepts/data-attributes). Your scripts handle the creative
part: effects, transitions, canvas, SVG, [GSAP](/guides/gsap-animation)
animation.
<Warning>
Never use a script to play, pause, or seek a media element, and never use one
to show or hide a clip based on time. The framework already does that from the
data attributes, and a script doing it too will fight the framework. See
[Common Mistakes](/guides/common-mistakes) for what that looks like.
</Warning>
## Reuse one composition with different content
One source file can appear several times in the same video, each copy carrying
its own text and colors. HyperFrames does not wire `data-var-*` attributes into
your DOM or CSS for you — you do it in three steps:
1. Declare each variable — id, type, default — on the sub-composition's root
with `data-composition-variables`. That root is the `<html>` element in a
full-document composition, or the `[data-composition-id]` element in a
template or fragment.
2. Pass each copy's values on its host element with `data-variable-values`.
3. Read them inside the composition with
`window.__hyperframes.getVariables()`, which layers the host's values over
the declared defaults, one copy at a time.
```html index.html
<div
data-composition-id="card-pro"
data-composition-src="compositions/card.html"
data-start="0"
data-track-index="1"
data-variable-values='{"title":"Pro","color":"#ff4d4f"}'
></div>
<div
data-composition-id="card-enterprise"
data-composition-src="compositions/card.html"
data-start="card-pro"
data-track-index="1"
data-variable-values='{"title":"Enterprise","color":"#22c55e"}'
></div>
```
The second card's `data-start="card-pro"` means "start when that one ends".
And the source file both cards share:
```html compositions/card.html
<html data-composition-variables='[
{"id":"title","type":"string","label":"Title","default":"Fallback"},
{"id":"color","type":"color","label":"Color","default":"#111827"}
]'>
<body>
<div data-composition-id="card" data-width="1920" data-height="1080">
<h1 class="title"></h1>
<style>
[data-composition-id="card"] .title { color: var(--card-color, #111827); }
</style>
<script>
const { title, color } = __hyperframes.getVariables();
const root = document.querySelector('[data-composition-id="card"]');
root.querySelector(".title").textContent = title;
root.style.setProperty("--card-color", color);
</script>
</div>
</body>
</html>
```
[Variables](/concepts/variables) covers the types, the bindings that need no
script, CLI overrides, and which value wins. If you are building tooling on
`@hyperframes/core`, `extractCompositionMetadata()` reads the same
`data-composition-variables` array — that is how Studio builds its editing UI.
## See every composition in a project
```bash
npx hyperframes compositions
```
## Related topics
- [Data attributes](/concepts/data-attributes)
- [Deterministic rendering](/concepts/determinism)
- [Variables](/concepts/variables)
- [Animate with GSAP](/guides/gsap-animation)
- [HTML schema reference](/reference/html-schema)
- [Start from an example](/examples)