| title | Variables |
|---|---|
| description | Parameterize compositions so the same source can render different 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.
Add data-composition-variables to the <html> root of any composition. Its value is a JSON array of variable declarations — one object per variable:
<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.
| Type | default value |
Extra options |
|---|---|---|
string |
"some text" |
placeholder?: string, maxLength?: number |
number |
0 |
min?: number, max?: number, step?: number, unit?: string |
color |
"#rrggbb" |
— |
boolean |
true / false |
— |
enum |
one of the option values | options: [{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.
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.
The same composition can render different images, video clips, or audio tracks just by swapping URLs through a string variable:
<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 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 scansvideo[data-start]elements after your script runs and reads the resolvedsrcfor 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.
Media color grading can also read exact variable references inside
data-color-grading. Use $name or ${name} as the entire value for a field;
the runtime resolves it from the current composition's variables before applying
the shader grading, finishing details, blur/pixelate effects, and optional LUT:
<html data-composition-variables='[
{"id":"gradingPreset","type":"enum","label":"Preset","default":"natural-lift",
"options":[{"value":"natural-lift","label":"Natural Lift"},{"value":"warm-daylight","label":"Warm Daylight"}]},
{"id":"gradingIntensity","type":"number","label":"Preset strength","default":0.75,"min":0,"max":1,"step":0.05},
{"id":"gradingExposure","type":"number","label":"Exposure","default":0,"min":-2,"max":2,"step":0.05},
{"id":"gradingVibrance","type":"number","label":"Vibrance","default":0.08,"min":-1,"max":1,"step":0.01}
]'>
<body>
<div data-composition-id="hero" data-width="1920" data-height="1080">
<video
id="hero-video"
src="assets/hero.mp4"
data-start="0"
data-track-index="0"
muted
playsinline
data-color-grading='{
"preset":"$gradingPreset",
"intensity":"$gradingIntensity",
"adjust":{"exposure":"${gradingExposure}","vibrance":"$gradingVibrance"},
"details":{"vignette":0.15,"vignetteFeather":0.72,"grain":0.08,"grainSize":0.25},
"effects":{"blur":0.1},
"colorSpace":"rec709"
}'
></video>
</div>
</body>
</html>When the same composition is embedded multiple times, each host's
data-variable-values can produce different grading without copying or rewriting
the media element's data-color-grading JSON.
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:
<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:
<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 a clip's data-duration from the live DOM after your script runs, so an attribute written programmatically onto a clip or media element behaves identically to one baked into the source HTML.
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:
| What | Mechanism (not a variable) |
|---|---|
| Composition dimensions | data-width / data-height on the composition element, parsed from the source HTML at compile time, not from the live DOM |
| Root composition total duration | data-duration on the root composition element (the total render length / frame count), parsed from the source HTML at compile time. A static root data-duration is locked before scripts run, so neither a script nor a variable can change the render length. (A clip's own data-duration is different: it is re-read from the live DOM, as shown above.) |
| 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 variables | Variables 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 (composition dimensions, and the root composition's total duration / render length) or that live entirely outside the composition (CLI flags, encoder settings).
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:
<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.
For the common cases — replaceable media, dynamic text, and CSS-driven styling — you don't need a script at all. The runtime resolves these bindings once at load, identically in preview and render:
-
data-var-src="id"— sets the element'ssrcfrom the variable value (a URL string, or an image value's{url}). The authoredsrcstays as the fallback when the variable resolves to nothing:<img class="clip" data-start="0" data-duration="5" data-var-src="heroImage" src="fallback.jpg" />
-
data-var-text="id"— sets the element's text content from a scalar variable:<h1 class="clip" data-start="0" data-duration="5" data-var-text="title">Fallback title</h1>
-
CSS custom properties — every scalar variable is applied as
--{id}on its composition root (font values apply their family name), so plain CSS bindings respond to render/preview overrides:.card-title { color: var(--accent); font-family: var(--brandFont), sans-serif; }
Bindings resolve against the element's owning composition, so sub-composition instances see their own per-instance values. The Studio Variables panel counts these bindings as usage. Use getVariables() in a script only when you need logic beyond direct substitution (loops, conditionals, derived values).
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:
<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.
Pass variable values at render time with --variables or --variables-file. These override the declared defaults for the top-level composition:
# 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.
Use --batch when the same composition should render once per data row:
[
{ "name": "Alice", "title": "Q4 Report" },
{ "name": "Bob", "title": "Renewal Plan" }
]npx hyperframes render --batch rows.json --output "renders/{name}.mp4" --strict-variablesEach row is treated like a --variables object and merged over the composition defaults. Output paths support {key} placeholders from the row plus {index}. Hyperframes validates missing placeholders, output collisions, and --strict-variables issues before the first row starts rendering, then writes manifest.json next to the outputs with one status row per render.
For small compositions, --batch-concurrency 2 can run rows in parallel. The default is 1 because each individual render already parallelizes across render workers.
Variable values are resolved by merging three sources, lowest to highest precedence:
| Source | Precedence | Where declared |
|---|---|---|
| Declared defaults | Lowest | data-composition-variables on <html> |
| Per-instance host overrides | Middle | data-variable-values on the sub-comp host element |
CLI --variables flag |
Highest | hyperframes 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.
The linter checks variable declarations statically:
npx hyperframes lintIt 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
--variableshas no matchingidindata-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
optionslist
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 Variables panel uses to build its editor for each composition.
The Studio's Variables tab (right inspector panel) is a full UI over this system:
- Declare and edit — add, edit, and remove declarations without touching the
HTML by hand; edits persist into
data-composition-variableswith undo support. - Preview with values — type-appropriate inputs write ephemeral overrides that
are injected into the preview as
window.__hfVariables, exactly like render-time injection, so what you preview is what--variablesrenders. A header pill shows whether you're previewing defaults or custom values. - Render with values — renders started from the Renders tab carry the active preview overrides.
- Handoff — copy the effective values as JSON or as a ready-to-run
hyperframes render --variablescommand. - Usage — declarations no script reads are badged
unused; ids read by scripts but missing from the schema get a one-click Declare action.