Skill Tracer
Skills › HyperFrames › remotion-to-hyperframes › parameters.md

parameters.md

172 lines · skills/remotion-to-hyperframes/references/parameters.md at a595755

Open on GitHub
1# Parameter translation: Zod schemas, defaultProps, calculateMetadata
2
3How a typed Remotion `<Composition schema={...} defaultProps={...} />`
4turns into a parameterized HF composition.
5

Parameter translation: Zod schemas, defaultProps, calculateMetadata

How a typed Remotion <Composition schema={...} defaultProps={...} /> turns into a parameterized HF composition.

Raw lines 1 to 5
6## Sync calculateMetadata (translatable)
7
8```tsx
9<Composition
10 id="MyVideo"
11 component={MyVideo}
12 schema={z.object({ title: z.string(), duration: z.number() })}
13 defaultProps={{ title: "Hello", duration: 90 }}
14 calculateMetadata={({ props }) => ({
15 durationInFrames: props.duration,
16 fps: 30,
17 })}
18/>
19```
21When `calculateMetadata` is synchronous and only uses `props`, **resolve
22it at translation time** — call it with `defaultProps` (or whatever the
23caller specifies) and write the concrete result into the HTML:
25```html
26<div
27 id="stage"
28 data-composition-id="MyVideo"
29 data-start="0"
30 data-duration="3" <!-- 90/30 -->
31 data-fps="30"
32 data-title="Hello"
33></div>
34```
36The `data-title` attribute carries the value through. Code that originally
37read `props.title` reads `document.getElementById("stage").dataset.title`
38in HF.
40Inside a scene sub-composition file that lookup does not reach the root
41`#stage`. Pass a prop to a scene with `data-variable-values` on its host and
42read it there with `window.__hyperframes.getVariables()` (declare defaults with
43`data-composition-variables` on the scene's composition element).

Sync calculateMetadata (translatable)

<Composition
  id="MyVideo"
  component={MyVideo}
  schema={z.object({ title: z.string(), duration: z.number() })}
  defaultProps={{ title: "Hello", duration: 90 }}
  calculateMetadata={({ props }) => ({
    durationInFrames: props.duration,
    fps: 30,
  })}
/>

When calculateMetadata is synchronous and only uses props, resolve it at translation time — call it with defaultProps (or whatever the caller specifies) and write the concrete result into the HTML:

<div
  id="stage"
  data-composition-id="MyVideo"
  data-start="0"
  data-duration="3"          <!-- 90/30 -->
  data-fps="30"
  data-title="Hello"
></div>

The data-title attribute carries the value through. Code that originally read props.title reads document.getElementById("stage").dataset.title in HF.

Inside a scene sub-composition file that lookup does not reach the root #stage. Pass a prop to a scene with data-variable-values on its host and read it there with window.__hyperframes.getVariables() (declare defaults with data-composition-variables on the scene's composition element).

Raw lines 6 to 44
45## Async calculateMetadata (NOT translatable)
47```tsx
48<Composition
49 calculateMetadata={async ({ props }) => {
50 const res = await fetch(...);
51 return { durationInFrames: res.duration };
52 }}
53/>
54```
56**Refuse + interop**. HF needs composition metadata up-front to seed the
57HTML. Resolving network calls at translation time defeats the purpose
58of having dynamic metadata. See [escape-hatch.md](escape-hatch.md).
60The lint rule `r2hf/async-metadata` catches this. T4 case 03 tests it.

Async calculateMetadata (NOT translatable)

<Composition
  calculateMetadata={async ({ props }) => {
    const res = await fetch(...);
    return { durationInFrames: res.duration };
  }}
/>

Refuse + interop. HF needs composition metadata up-front to seed the HTML. Resolving network calls at translation time defeats the purpose of having dynamic metadata. See escape-hatch.md.

The lint rule r2hf/async-metadata catches this. T4 case 03 tests it.

Raw lines 45 to 61
62## Default props
64```tsx
65defaultProps={{
66 title: "Hello",
67 subtitle: "World",
68 count: 42,
69}}
70```
72Translate to `data-*` attributes on the root `#stage` div:
74```html
75<div id="stage" data-title="Hello" data-subtitle="World" data-count="42">...</div>
76```
78Convention: `propName` → `data-prop-name` (kebab-case). Inside the GSAP
79script, read via `document.getElementById("stage").dataset.propName`.

Default props

defaultProps={{
  title: "Hello",
  subtitle: "World",
  count: 42,
}}

Translate to data-* attributes on the root #stage div:

<div id="stage" data-title="Hello" data-subtitle="World" data-count="42">...</div>

Convention: propName → data-prop-name (kebab-case). Inside the GSAP script, read via document.getElementById("stage").dataset.propName.

Raw lines 62 to 80
81## Nested object / array props
83```tsx
84defaultProps={{
85 stats: [
86 { label: "Stars", value: 1247, color: "#fbbf24" },
87 { label: "Forks", value: 312, color: "#60a5fa" },
88 ],
89}}
90```
92Don't try to encode the array as a JSON `data-` attribute — HF's runtime
93doesn't parse those. Materialize the array as **repeated HTML markup**:
95```html
96<div id="scene-stats">
97 <div class="stat-card" data-stat-index="0" data-stat-value="1247" style="--card-color:#fbbf24">
98 <div class="number">0</div>
99 <div class="label">Stars</div>
100 </div>
101 <div class="stat-card" data-stat-index="1" data-stat-value="312" style="--card-color:#60a5fa">
102 <div class="number">0</div>
103 <div class="label">Forks</div>
104 </div>
105</div>
106```
108The component template (`StatCard.tsx`) becomes the markup template;
109each instance gets its scalar props rendered as `data-*` and CSS
110custom properties.
112Validated in T3 — three StatCards reused with different props,
113mean SSIM 0.953.

Nested object / array props

defaultProps={{
  stats: [
    { label: "Stars", value: 1247, color: "#fbbf24" },
    { label: "Forks", value: 312, color: "#60a5fa" },
  ],
}}

Don't try to encode the array as a JSON data- attribute — HF's runtime doesn't parse those. Materialize the array as repeated HTML markup:

<div id="scene-stats">
  <div class="stat-card" data-stat-index="0" data-stat-value="1247" style="--card-color:#fbbf24">
    <div class="number">0</div>
    <div class="label">Stars</div>
  </div>
  <div class="stat-card" data-stat-index="1" data-stat-value="312" style="--card-color:#60a5fa">
    <div class="number">0</div>
    <div class="label">Forks</div>
  </div>
</div>

The component template (StatCard.tsx) becomes the markup template; each instance gets its scalar props rendered as data-* and CSS custom properties.

Validated in T3 — three StatCards reused with different props, mean SSIM 0.953.

Raw lines 81 to 114
115## Numeric props that need typed parsing
117`document.getElementById("stage").dataset.count` is a string. Convert at
118read time:
120```js
121const count = Number(stage.dataset.count);
122```
124Or inline values directly into the GSAP script when the data is known
125at translation time and doesn't need to vary per render.

Numeric props that need typed parsing

document.getElementById("stage").dataset.count is a string. Convert at read time:

const count = Number(stage.dataset.count);

Or inline values directly into the GSAP script when the data is known at translation time and doesn't need to vary per render.

Raw lines 115 to 126
127## Boolean props
129```tsx
130defaultProps={{ darkMode: true }}
131```
133Two conventions:
135- `data-dark-mode="true"` — read as string, compare `=== "true"`
136- `data-dark-mode` (presence/absence) — `<div data-dark-mode>` for true, omit for false
138Pick one and be consistent. The presence/absence form is HTML-idiomatic
139and pairs well with CSS attribute selectors:
141```css
142[data-dark-mode] .scene {
143 background: #000;
145```

Boolean props

defaultProps={{ darkMode: true }}

Two conventions:

  • data-dark-mode="true" — read as string, compare === "true"
  • data-dark-mode (presence/absence) — <div data-dark-mode> for true, omit for false

Pick one and be consistent. The presence/absence form is HTML-idiomatic and pairs well with CSS attribute selectors:

[data-dark-mode] .scene {
  background: #000;
}
Raw lines 127 to 146
147## Zod runtime validation
149Remotion's `schema` validates props at composition load. HF doesn't have
150an equivalent — by the time the HTML is in the renderer, the schema is
151already gone.
153Validate at translation time instead. If the user passes invalid data,
154fail with a translation error before emitting HTML. This matches Zod's
155"fail loud" intent without requiring the runtime dependency.

Zod runtime validation

Remotion's schema validates props at composition load. HF doesn't have an equivalent — by the time the HTML is in the renderer, the schema is already gone.

Validate at translation time instead. If the user passes invalid data, fail with a translation error before emitting HTML. This matches Zod's "fail loud" intent without requiring the runtime dependency.

Raw lines 147 to 156
157## When the composition uses props for computed prop derivation
159```tsx
160const Composition: React.FC<Props> = ({ stats }) => {
161 const total = stats.reduce((acc, s) => acc + s.value, 0);
162 return <div>{total}</div>;
163};
164```
166Compute the derived value at translation time and bake it into the HTML
167or a `data-` attribute. Don't try to express the computation in JS in the
168HF composition — that adds runtime overhead and makes the HTML stateful
169in ways that complicate human editing.
171If the derivation is non-trivial (involves the array itself, not just
172scalars), materialize it as static text in the HTML.

When the composition uses props for computed prop derivation

const Composition: React.FC<Props> = ({ stats }) => {
  const total = stats.reduce((acc, s) => acc + s.value, 0);
  return <div>{total}</div>;
};

Compute the derived value at translation time and bake it into the HTML or a data- attribute. Don't try to express the computation in JS in the HF composition — that adds runtime overhead and makes the HTML stateful in ways that complicate human editing.

If the derivation is non-trivial (involves the array itself, not just scalars), materialize it as static text in the HTML.

Raw lines 157 to 172