Skip to main content
Variables let you declare named, typed slots in a composition and fill them at render time — from a parent composition, from the CLI, or from an API call. A card composition that takes title and color can be embedded a hundred times with a hundred different values without duplicating any HTML.

Declaring Variables

Add data-composition-variables to the <html> root of any composition. Its value is a JSON array of variable declarations — one object per variable:
compositions/card.html
<html data-composition-variables='[
  {"id":"title",  "type":"string",  "label":"Title",  "default":"Hello"},
  {"id":"color",  "type":"color",   "label":"Color",  "default":"#111827"},
  {"id":"price",  "type":"number",  "label":"Price",  "default":0,        "unit":"$"},
  {"id":"featured","type":"boolean","label":"Featured","default":false},
  {"id":"plan",   "type":"enum",    "label":"Plan",   "default":"pro",
   "options":[{"value":"pro","label":"Pro"},{"value":"enterprise","label":"Enterprise"}]}
]'>
Every declaration requires four fields: id, type, label, and default. id must be unique within the composition.

Variable Types

Typedefault valueExtra options
string"some text"placeholder?: string, maxLength?: number
number0min?: number, max?: number, step?: number, unit?: string
color"#rrggbb"
booleantrue / false
enumone of the option valuesoptions: [{value: string, label: string}]
The Studio editing UI uses label, type, and the type-specific options to render the right input widget for each variable.

What can be a variable

Variables come in two layers. The five declared types above cover typed primitive data — strings, numbers, colors, booleans, enums. For everything else, a string variable holding a URL is the escape hatch: your composition reads the URL and assigns it to whatever DOM element needs it.

Parameterizing media assets

The same composition can render different images, video clips, or audio tracks just by swapping URLs through a string variable:
compositions/product-card.html
<html data-composition-variables='[
  {"id":"productImage","type":"string","label":"Product image URL","default":"https://cdn.example.com/products/default.png"},
  {"id":"productName","type":"string","label":"Product name","default":"Untitled"}
]'>
  <body>
    <div data-composition-id="product-card" data-width="1920" data-height="1080" data-duration="5">
      <img class="product-img" alt="" />
      <h1 class="product-name"></h1>

      <script>
        const {
          productImage = "https://cdn.example.com/products/default.png",
          productName = "Untitled",
        } = __hyperframes.getVariables();
        const root = document.querySelector('[data-composition-id="product-card"]');
        root.querySelector(".product-img").src = productImage;
        root.querySelector(".product-name").textContent = productName;
      </script>
    </div>
  </body>
</html>
The runtime probes the DOM after your composition script runs, so a <video> or <audio> src assigned at runtime from a variable is discovered and pre-extracted for the render. No extra wiring required — just set the src from your variable.
The same pattern covers the three media element types:
  • <img src> — assign from a string variable. Chrome fetches it during capture like any other image; no extra config.
  • <video src> — assign from a string variable, but keep the timing attributes (data-start, data-duration, data-track-index, data-has-audio) on the element itself. The probe phase scans video[data-start] elements after your script runs and reads the resolved src for pre-extraction.
  • <audio src> — same as video. The audio is decoded during capture and mixed into the final output.
Pass assets as URL references your composition resolves at render time; don’t inline base64. URL-shaped assets travel cleanly through both the local renderer and the Lambda surface — see Templates on Lambda for the 256 KiB execution-input cap on distributed renders.

Swapping media: do you need to vary duration too?

A common follow-up: if a variable swaps a <video> to a different clip, does data-duration need to change too? Usually no. data-duration is optional on <video> and <audio> — leave it off and the renderer ffprobes the source and uses its natural length:
compositions/hero.html
<video id="hero" data-start="0" data-track-index="0"></video>
<script>
  document.getElementById("hero").src = __hyperframes.getVariables().heroVideo;
</script>
If you need to clamp or pin the clip to a specific length per render — for example, to keep downstream timing stable across clips of different source lengths — expose duration as its own number variable and apply it via the same script:
compositions/hero.html
<video id="hero" data-start="0" data-track-index="0"></video>
<script>
  const { heroVideo, heroDuration } = __hyperframes.getVariables();
  const el = document.getElementById("hero");
  el.src = heroVideo;
  if (heroDuration !== undefined) {
    el.setAttribute("data-duration", String(heroDuration));
  }
</script>
The probe phase reads data-duration from the live DOM after your script runs, so an attribute written programmatically behaves identically to one baked into the source HTML.

What can’t be a variable

A small set of inputs are read once from the source HTML or from the CLI / SDK, with no live-DOM re-read — no script (and therefore no variable) can change them:
WhatMechanism (not a variable)
Composition dimensionsdata-width / data-height on the composition element — parsed from the source HTML at compile time, not from the live DOM
Frame rate--fps flag on hyperframes render, or config.fps in the SDK
Output format / codec / quality--format / --codec / --quality flags, or the SDK equivalents
A sibling or parent composition’s variablesVariables are per-composition; use data-variable-values on each sub-comp host element to pass overrides
The deeper rule: variables are runtime values your script applies to the DOM. They can drive anything the renderer reads from the live DOM after that script runs — text, colors, media src, even clip data-duration as shown above. They can’t change inputs the renderer reads once at compile time (dimensions) or that live entirely outside the composition (CLI flags, encoder settings).

Reading Variables at Runtime

Inside any composition script, call window.__hyperframes.getVariables() to get the resolved variable values. The return type is Partial<Record<string, unknown>> — use destructuring with defaults matching the declared default values:
compositions/card.html
<html data-composition-variables='[
  {"id":"title","type":"string","label":"Title","default":"Untitled"},
  {"id":"color","type":"color","label":"Color","default":"#111827"}
]'>
  <body>
    <div data-composition-id="card" data-width="1920" data-height="1080">
      <h1 class="card-title"></h1>

      <style>
        [data-composition-id="card"] { --card-color: #111827; }
        [data-composition-id="card"] .card-title { color: var(--card-color); }
      </style>

      <script>
        const { title = "Untitled", color = "#111827" } = __hyperframes.getVariables();
        const root = document.querySelector('[data-composition-id="card"]');
        root.querySelector(".card-title").textContent = title;
        root.style.setProperty("--card-color", color);
      </script>
    </div>
  </body>
</html>
__hyperframes.getVariables() is a shorthand for window.__hyperframes.getVariables() and works in both top-level and sub-composition scripts. The runtime automatically scopes sub-compositions so each instance sees its own resolved values.

Per-instance Overrides (Sub-compositions)

When embedding a composition inside another, use data-variable-values on the host element to pass a JSON object of override values for that particular instance:
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>
Both host elements point to the same card.html source, but each instance receives different values. The runtime merges the host’s data-variable-values over the sub-comp’s declared defaults on a per-instance basis — the same sub-composition can run with completely different content simultaneously.

CLI Overrides (Top-level Renders)

Pass variable values at render time with --variables or --variables-file. These override the declared defaults for the top-level composition:
Terminal
# Inline JSON
npx hyperframes render --variables '{"title":"Q4 Report","color":"#1d4ed8"}' --output q4.mp4

# JSON file
npx hyperframes render --variables-file ./vars.json --output out.mp4

# Fail on undeclared or mistyped variables
npx hyperframes render --variables '{"title":"Q4 Report"}' --strict-variables --output out.mp4
--strict-variables turns variable warnings into errors. Any variable in --variables that is not declared in data-composition-variables, or whose value does not match the declared type, causes the render to exit non-zero. Useful in CI pipelines where an undeclared variable key likely indicates a typo or a schema mismatch.
CLI overrides apply only to the top-level composition. Sub-composition variables are controlled by data-variable-values on each host element.

Layering and Precedence

Variable values are resolved by merging three sources, lowest to highest precedence:
SourcePrecedenceWhere declared
Declared defaultsLowestdata-composition-variables on <html>
Per-instance host overridesMiddledata-variable-values on the sub-comp host element
CLI --variables flagHighesthyperframes render --variables '{...}'
A missing key at any layer falls through to the next lower layer. If no layer provides a value, the declared default is used.

Validation

The linter checks variable declarations statically:
Terminal
npx hyperframes lint
It catches malformed JSON, missing required fields (id, type, label, default), and type mismatches between type and the default value. Fix lint errors before rendering — they indicate the runtime will be unable to resolve variables correctly. At render time, the CLI validates --variables against the schema and reports issues as warnings (or errors with --strict-variables):
  • undeclared — a key in --variables has no matching id in data-composition-variables
  • type-mismatch — the value’s JavaScript type does not match the declared type (e.g. a string where a number is expected)
  • enum-out-of-range — an enum value is not in the declared options list

Inspecting Variables Programmatically

If you are building tooling on top of @hyperframes/core, the variable declarations are readable without rendering:
import { extractCompositionMetadata } from "@hyperframes/core";
import { readFileSync } from "node:fs";

const html = readFileSync("compositions/card.html", "utf8");
const { variables } = extractCompositionMetadata(html);
// variables is CompositionVariable[]
This is the same API the Studio editing UI uses to build the variables panel for each composition.

Next Steps

Data Attributes

Full reference for data-composition-variables and data-variable-values attributes

Compositions

How nested compositions use variables for reuse

Rendering

CLI flags for passing variables at render time

CLI Reference

All CLI commands and flags