> ## Documentation Index
> Fetch the complete documentation index at: /llms.txt
> Use this file to discover all available pages before exploring further.

# Compositions

> 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.md).

## 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.md) page lists every timing
attribute. The [HTML schema](/reference/html-schema.md) 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.

> [tab] 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>
> ```
>
> Leading HTML comments are allowed before `<template>`. Targeting this scene
> with `render -c compositions/intro-anim.html` or
> `keyframes compositions/intro-anim.html --shot scene.png` reuses its mount
> from `index.html`, including the project's library scripts.
>
> `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.
>
> Timing inside the child file is local to that composition. If this host
> starts at `10` and a child video starts at `2`, the video starts at `12` on
> the root timeline. Legacy projects that authored a media start in root time
> can preserve it explicitly with `data-hf-media-start-basis="global"`; new
> compositions should use local timing.

> [tab] 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.

### Where the files live

**project**

**index.html**

**compositions**

**intro-anim.html**

**caption-overlay.html**

**outro-title.html**

**assets**

**video.mp4**

**music.mp3**

**logo.png**

## 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.md). Your scripts handle the creative
part: effects, transitions, canvas, SVG, [GSAP](/guides/gsap-animation.md)
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](/help.md) for what that looks like.

## 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-duration="3"
  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-duration="3"
  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.md) 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 the browser editor builds its editing UI.

## Load a composition's own files from its script

When a composition is mounted, a `src` or `href` attribute that points at a
file next to it is rewritten to where that file lives, so
`<img src="assets/logo.png">` keeps working. A path your script builds is a
plain string and is never rewritten, so resolve it with
`window.__hyperframes.assetUrl()`. It takes a path written relative to the
composition's own file and returns a URL the page can load, whether the file is
mounted, previewed or opened on its own.

A path meant from the project root, such as `assets/logo.png` in a composition
at `compositions/intro.html` whose image sits in the project's own `assets/`
folder, already resolves from the page. Leave it as a plain string:
`assetUrl()` would look for it beside the composition and miss.

```html compositions/globe/globe.html
<div data-composition-id="globe" data-width="1920" data-height="1080">
  <script>
    const texture = new THREE.TextureLoader().load(
      __hyperframes.assetUrl("assets/earth.jpg"),
    );
  </script>
</div>
```

A separate script file (`<script src="...">`) has no composition of its own to
resolve against. Resolve the URLs in the composition's inline script and hand
them over, for example on a global the file reads.

## See every composition in a project

```bash
npx hyperframes compositions
```

## Related topics

* [Data attributes](/concepts/data-attributes.md)
* [Deterministic rendering](/concepts/determinism.md)
* [Variables](/concepts/variables.md)
* [Animate with GSAP](/guides/gsap-animation.md)
* [HTML schema reference](/reference/html-schema.md)
* [Start from an example](/examples.md)
