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

# Lint rules

> Every lint finding code with its severity, what it catches, and how to fix it.

`hyperframes lint`, `hyperframes check` and the `@hyperframes/lint` package report findings with a stable code. This page lists every code with its severity, what it catches, and how to fix it. The codes come straight from the rule set in `packages/lint/src/rules/` plus the project-level probes that run as part of a project lint.

Severities:

* **Error** — the composition is broken or the render will fail or visibly diverge. Fails `hyperframes check` by default.
* **Warning** — a quality, determinism or maintainability hazard. Does not fail `hyperframes check` unless you pass `--strict`; in `render`, warnings abort only under `--strict-all`.
* **Info** — a heads-up that needs no action. Shown by `hyperframes lint --verbose`.

<Info>
  Looking for a code from a render or check log? Codes appear in the output of `npx hyperframes lint [dir]` — add `--json` to script over them. `check` additionally runs runtime, layout and motion audits beyond these lint rules; see [Troubleshooting](/guides/troubleshooting).
</Info>

## Structure and timeline

Source: `packages/lint/src/rules/structure.ts`.

The four codes marked **Warning or error** are warnings when the CLI lints a project, and errors when Studio lints the file it is editing.

### `nested_structure_needs_subcomposition`

**Warning or error.** A top-level timeline element contains another nested structure instead of a sub-composition mount. The timeline draws one row per top-level element, and the root composition is built only from sub-compositions.

**Fix:** Move the element and its contents into a sub-composition file and mount it with `data-composition-src`.

### `timeline_element_missing_timing`

**Warning or error.** A timeline element has no `data-duration`, so the timeline cannot draw where it ends.

**Fix:** Add `data-duration` (in seconds) to the element.

### `caption_track_kind_missing`

**Warning or error.** An element looks like a caption row but is not marked as one, so the timeline cannot group it on the caption track.

**Fix:** Add `data-track-kind="captions"` to the element.

### `multiple_caption_tracks`

**Warning or error.** Captions are spread across several timeline tracks instead of one caption lane.

**Fix:** Give every caption element the same `data-track-index`.

### `clip_ends_past_root_duration`

**Warning.** A clip runs past the root composition's `data-duration`, so it is cut off in previews, posters and renders.

**Fix:** Extend the root `data-duration`, or make the clip end at or before it.

## Composition contract

Source: `packages/lint/src/rules/composition.ts`.

### `invalid_composition_variables_declaration`

**Error.** `data-composition-variables` is not valid JSON or not an array of variable declarations.

**Fix:** Provide a JSON array such as `data-composition-variables='[{"id":"title","type":"string","label":"Title","default":"Hello"}]'` — single-quote the attribute, double-quote the JSON.

### `invalid_variable_values_json`

**Error.** `data-variable-values` is not valid JSON.

**Fix:** Single-quote the attribute and double-quote the JSON keys and values, e.g. `data-variable-values='{"title":"Hello"}'`.

### `unknown_variable_binding`

**Warning.** An element binds a variable that is not declared in `data-composition-variables`, so the binding silently keeps the authored fallback.

**Fix:** Declare the variable on the composition root (`<html>`, or the `data-composition-id` root of a template or fragment composition).

### `unloadable_media_variable_default`

**Error.** A variable's default is a URL the runtime refuses to load, so bound elements render their authored fallback src while the render still exits 0.

**Fix:** Use a relative path, `http(s)`, `blob:`, or a `data:image/*` URI. Copy the file into the project and reference it relatively.

### `deprecated_data_end`

**Error.** A legacy `data-end` is used without `data-duration`, or a stale `data-end` disagrees with the `data-duration` next to it.

**Fix:** Replace `data-end` with `data-duration`. The compiler generates `data-end` from `data-duration` automatically.

### `deprecated_data_layer`

**Error.** The legacy `data-layer` attribute is used.

**Fix:** Replace it with `data-track-index`, the canonical name Studio and the linter read.

### `duplicate_composition_id`

**Error.** The same `data-composition-id` appears on several elements in one file — often a `<meta>` tag carrying the same id as the root `<div>`.

**Fix:** Keep `data-composition-id` on exactly one element: the composition root. Remove it from metadata or duplicate hosts.

### `html_dir_attribute_breaks_render`

**Error.** `<html dir="rtl">` or `<html dir="auto">` renders correctly in preview and snapshot but produces a fully blank or black video from render — a confirmed, silent failure. `dir="ltr"` is not flagged.

**Fix:** Remove `dir` from `<html>`, keep `lang`, and scope the direction to individual text-containing elements instead — `direction: rtl` in CSS, or `dir="auto"` on the element.

### `invalid_parent_traversal_in_asset_path`

**Error.** Asset paths use `../` to traverse above the project root; render and preview clamp or misresolve them.

**Fix:** Point the path at a file inside the project — from `compositions/scene.html`, both `../assets/x.png` and `assets/x.png` reach the project's `assets` folder.

### `composition_exceeds_inspection_viewport_cap`

**Warning.** The composition is larger than the 4096px viewport cap that `check`, `validate`, `snapshot`, `compare` and layout audits clamp to. `render` is unaffected, but a clean inspection result no longer covers the whole canvas.

**Fix:** Keep the authored size if the output needs it, and verify the region past the cap from a render rather than from inspection commands.

### `composition_file_too_large`

**Warning.** The HTML composition file has grown past a size where it stays easy to read, iterate on, and diff.

**Fix:** Split it into sub-compositions and mount them from the parent with `data-composition-src`.

### `composition_heavy_overlay_count_high`

**Warning.** Many elements carry heavy overlay CSS (`filter: blur`, `radial-gradient`, `clip-path`), which can cap capture performance — including hidden ones.

**Fix:** Split the composition and concat the pieces so each capture sees a small subset of heavy overlays; remove overlays that are not visible anyway.

### `missing_data_no_timeline`

**Warning.** A `data-composition-id` element (or the root) has neither a matching `window.__timelines` registration nor `data-no-timeline`, so the producer polls up to 45 seconds before timing out.

**Fix:** Register the timeline, mount the composition with `data-composition-src`, or add `data-no-timeline` for genuinely static sections.

### `negative_z_index`

**Warning.** `z-index` below 0 paints the element behind its nearest stacking context's content; with no stacking-context ancestor that is the composition root, so an opaque background there hides the element entirely.

**Fix:** Give the element's parent a stacking context (`isolation: isolate` is the cheapest; `transform`, `filter`, `opacity` below 1, `contain: paint`, `will-change` also work).

### `requestanimationframe_in_composition`

**Error.** `requestAnimationFrame` runs on wall-clock time, not the GSAP timeline, so it desyncs from frame capture and can flicker or drop frames in renders.

**Fix:** Use GSAP tweens or timeline `onUpdate` callbacks for animation logic instead.

### `root_composition_duration_derived`

**Warning.** The root has no `data-duration` and no GSAP timeline, so its length is inferred from its clips.

**Fix:** Add `data-duration="<seconds>"` to the root element to set the length yourself.

### `root_composition_missing_duration_source`

**Error.** The root has no `data-duration`, no GSAP timeline, and no CSS, WAAPI, Lottie or Three.js animation to infer a duration from, so the render engine cannot determine how long to capture and fails.

**Fix:** Add `data-duration="<seconds>"` to the root, or add a paused GSAP timeline registered on `window.__timelines`.

### `root_composition_missing_html_wrapper`

**Error.** An `index.html` carrying `data-composition-id` is a bare fragment without `<!DOCTYPE html>`, `<html>` or `<body>`, so browsers quirks-mode it and the preview server mis-serves it.

**Fix:** Wrap the composition in `<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>...</body></html>`.

### `root_zoom_rescales_a_fixed_canvas`

**Error.** CSS `zoom` on the canvas rescales the painted composition inside a capture frame that does not rescale with it — the frame is sized from the root's `data-width` and `data-height` in layout pixels.

**Fix:** Author at the real size: set `data-width` and `data-height` (and the root's width and height) and drop the canvas-level `zoom`. To scale the output without changing layout, pass `--output-resolution` to render. `zoom` on descendants is fine.

### `split_data_attribute_selector`

**Error.** A CSS selector combines two attributes inside one `[...]` block, which browsers reject — GSAP timelines or `querySelector` calls fail before they register.

**Fix:** Use separate attribute selectors: `[data-composition-id="x"][data-track-index="1"]`.

### `standalone_composition_wrapped_in_template`

**Error.** The root `index.html` is wrapped in a `<template>` tag, so the runtime cannot play it — only sub-compositions loaded via `data-composition-src` use `<template>` wrappers.

**Fix:** Remove the `<template>` wrapper and serve the composition as a real document.

### `subcomposition_blanks_before_host`

**Warning.** A mounted sub-composition is blank for part of its host's window because its own `data-duration` is shorter than the slot it fills.

**Fix:** Set the sub-composition's `data-duration` to fill the host window, or cover the gap with another clip.

### `subcomposition_root_styled_by_class`

**Error.** A sub-composition's root element is styled through class selectors, but at render every rule is scoped to `[data-composition-id=...]`, so the class-keyed styles stop applying.

**Fix:** Give the root `id="root"` and style it with `#root` plus plain descendant selectors.

### `template_literal_selector`

**Error.** `querySelector` is called with a template-literal variable, which the bundler's CSS parser crashes on.

**Fix:** Use hardcoded string selectors instead of interpolated ones.

### `timed_element_missing_clip_class`

**Warning.** An element has timing attributes but no `class="clip"`. The runtime still times it, but Studio and the clip-ownership rules use `.clip` to recognise it, so it is harder to edit and to lint.

**Fix:** Add `class="clip"` to the element.

### `timeline_track_too_dense`

**Warning.** One track in this file carries more than 3 timed elements (audio, video, caption cues and sub-composition mounts are not counted), which makes the timeline harder to read, iterate on, and diff.

**Fix:** Split the elements into sub-compositions and mount them from the parent with `data-composition-src`.

## Document, CSS and scripts

Source: `packages/lint/src/rules/core.ts`.

### `host_missing_composition_id`

**Error.** A `data-composition-src` host element has no `data-composition-id` of its own.

**Fix:** Set `data-composition-id` on every `data-composition-src` host element.

### `css_parse_error`

**Error.** A `<style>` block fails to parse as CSS.

**Fix:** Fix the reported CSS syntax error.

### `css_transition_used`

**Warning.** A CSS `transition` declaration runs on the browser clock and cannot be seeked deterministically across render workers. `transition: none` and all-zero durations are allowed.

**Fix:** Keep the class or attribute swap for state, but put the visual change on the paused GSAP timeline.

### `id_override_reduced_specificity`

**Warning.** A selector sets position properties through `[id=...]` or `:where(#id)`, which has class-level or zero specificity, so a compound class rule on the same element can silently win.

**Fix:** Use the plain `#id` selector; id specificity beats any selector built only from classes.

### `id_requires_css_escape`

**Error / warning.** An id starts with a digit, so `#id` selectors throw a `SyntaxError` in `querySelector` and GSAP, and CSS drops the rule. Error when such a selector is actually used in the composition, warning otherwise.

**Fix:** Rename the id to start with a letter (recommended), or build selectors with `CSS.escape(id)` at runtime.

### `invalid_inline_script_syntax`

**Error.** An inline `<script>` is closed with malformed syntax.

**Fix:** Close inline scripts with a valid `</script>` tag.

### `missing_timeline_registry`

**Error.** The composition registers no `window.__timelines` registry at all.

**Fix:** Register each composition timeline on `window.__timelines[compositionId]`.

### `non_deterministic_code`

**Error.** A script uses a non-deterministic value source (`Math.random`, `Date.now`, `gsap.utils.random`, `"random(...)"`), so renders can differ between frames or runs.

**Fix:** Precompute the value once at build time and pass a constant, or seed and freeze it.

### `repeated_id_descendant_selector`

**Error.** A selector requires an id to be nested inside another element with the same id. Ids must be unique, so the selector can never match.

**Fix:** Remove the duplicate ancestor: change `#x #x` to `#x`.

### `root_dimensions_mismatch`

**Warning.** The root's `data-width`/`data-height` were edited directly, but the scaffold's `html, body` CSS and `<meta viewport>` still hold the old resolution, and a stale `overflow: hidden` body visually clips the correctly-sized root.

**Fix:** Update the body CSS and viewport to match, or scaffold with `hyperframes init --resolution portrait`.

### `root_missing_composition_id`

**Error.** The root composition is missing `data-composition-id`.

**Fix:** Add a stable `data-composition-id` to the entry composition wrapper.

### `root_missing_dimensions`

**Error.** The root composition is missing `data-width` or `data-height`.

**Fix:** Set numeric `data-width` and `data-height` on the entry composition root.

### `runtime_hidden_style_opacity`

**Error.** A selector observes the runtime-owned hidden style (used to hide native video during frame copying) and forces `opacity` to zero, which breaks the video replacement pipeline.

**Fix:** Restrict the guard to sub-composition hosts (for example `[data-composition-src][style*="visibility: hidden"]`) and do not derive element or media opacity from the runtime's hidden style.

### `scoped_css_missing_wrapper`

**Warning.** Scoped CSS targets a composition id for which no matching wrapper exists in this HTML.

**Fix:** Keep the matching composition wrapper, or align the CSS scope to an existing wrapper.

### `studio_missing_editable_id`

**Warning.** A timeline-visible element has no `id`, so Studio cannot target it stably for timeline and canvas edits.

**Fix:** Add a stable, human-readable id such as `id="hero-title"` to elements that agents or Studio should edit.

### `timeline_id_mismatch`

**Error.** A timeline is registered under a key for which no element carries the matching `data-composition-id`, so the runtime cannot auto-nest it.

**Fix:** Make `window.__timelines["<key>"]` match the element's `data-composition-id`, or vice versa.

### `unbalanced_style_tags`

**Error.** A `<style>` block is never closed (following markup is parsed as CSS and disappears), or an extra `</style>` closes the stylesheet early and dumps CSS into the body as visible text.

**Fix:** Keep `<style>` and `</style>` paired.

### `unclosed_tag_swallowed_element`

**Error.** A tag is missing its closing `>` before the next `<`, so the following element is swallowed as bogus attribute text and never becomes a real node.

**Fix:** Close the previous tag's `>` before opening the next element.

### `visible_markup_comment`

**Error.** CSS or JS block-comment syntax (`/* ... */`) appears in visible HTML markup, where only `<!-- ... -->` is a comment — so it renders as on-screen text.

**Fix:** Remove the text or convert it to a real HTML comment; keep CSS comments inside `<style>` and JS comments inside `<script>`.

## GSAP animation

Source: `packages/lint/src/rules/gsap.ts`.

### `audio_reactive_single_tween_per_group`

**Warning.** Audio-reactive captions use a single tween per group based on peak values, which sets one static value and is not perceptible as audio reactivity.

**Fix:** Sample the audio data at 100-200ms intervals across each group's lifetime and create a tween per sample point.

### `gsap_animates_clip_element`

**Error.** GSAP sets visibility-affecting properties (`opacity`, `visibility`, `autoAlpha`) directly on a clip element whose visibility the framework owns.

**Fix:** Remove the visibility or autoAlpha tween, or move the content into a child `<div>` and target that instead.

### `gsap_callback_dom_measurement`

**Warning.** A timeline callback reaches DOM measurement (`getBoundingClientRect`, `getTotalLength`, `getComputedStyle`, ...). Callbacks re-fire on every seek, and a cold render worker runs them against a different layout than the sequential pass.

**Fix:** Do all measurement and DOM setup synchronously at build time, before registering the timeline, or derive geometry from fixed composition coordinates.

### `gsap_cold_seek_hidden_fromto_missing_reveal`

**Error.** An element authored hidden uses `gsap.fromTo()` whose destination vars omit `opacity`/`autoAlpha`, so cold render workers restore the hidden state and the element can stay invisible.

**Fix:** Add `opacity: 1` (or `autoAlpha: 1`) to the destination vars so every seek path establishes the visible end state.

### `gsap_css_transform_conflict`

**Error.** An element has a CSS `transform` (for example `translateX(-50%)` centering) and a GSAP tween animates transform properties — GSAP overwrites the full CSS transform.

**Fix:** Remove the transform from CSS and express it in GSAP properties instead; `tl.fromTo` with explicit start values is exempt.

### `gsap_from_opacity_noop`

**Error.** An element has CSS `opacity: 0` and a `gsap.from()` that also sets opacity to 0, so it animates from 0 to 0 and never appears.

**Fix:** Remove `opacity: 0` from the CSS or inline style and let the `gsap.from()` own the initial hidden state.

### `gsap_fullscreen_overlay_starts_visible`

**Error.** A full-frame overlay starts visible before its first GSAP opacity tween, so it covers earlier render frames — often as a blank or white video.

**Fix:** Author the overlay hidden in CSS, or add an immediate `gsap.set(..., { opacity: 0 })` before the reveal tween.

### `gsap_function_value_hazard`

**Error / warning.** A function-valued tween var reads transform-sensitive geometry, measures layout at tween init, or accesses its first parameter (which is a number, not the element). Error for transform-sensitive reads and bad member access, warning otherwise.

**Fix:** Compute the value once at build time and pass a constant; for per-element logic use the second parameter: `(index, target) => ...`.

### `gsap_group_selector_keyframes`

**Warning.** A GSAP tween targets several elements with shared keyframes, so editing one element's keyframes in Studio changes all of them.

**Fix:** Replace the group selector with individual `tl.to()` calls per element.

### `gsap_infinite_repeat`

**Error / warning.** A GSAP tween or timeline uses `repeat: -1`. Warning when the composition declares a finite `data-duration` (seeking and export are clipped to it), error when nothing bounds the timeline.

**Fix:** Declare a finite `data-duration`, or replace `repeat: -1` with a finite count such as `repeat: Math.max(0, Math.floor(totalDuration / cycleDuration) - 1)`.

### `gsap_non_transform_motion`

**Error.** A GSAP tween animates layout properties (`left`, `top`, `width`, ...), text-reflow properties (`fontSize`, `letterSpacing`, ...) or sets `roundProps`, all of which snap to integer device pixels and stutter under frame capture.

**Fix:** Animate transforms instead: positional properties become `x`/`y`/`scale`; reflow properties need per-character splits or a static final value; remove `roundProps`.

### `gsap_relative_value_second_writer`

**Error.** A relative tween value (`+=...`) starts while another writer is still active on the same property, so different seek paths init the relative tween from different bases and the frame snaps.

**Fix:** Use absolute values or a `fromTo()` with explicit endpoints for that property. A relative value with a single writer is safe.

### `gsap_repeat_ceil_overshoot`

**Warning.** A repeat count computed with `Math.ceil` can overshoot the composition duration, extending the animation past it.

**Fix:** Use `Math.floor` in the repeat calculation: `repeat: Math.max(0, Math.floor(totalDuration / cycleDuration) - 1)`.

### `gsap_repeat_floor_unclamped`

**Warning.** A repeat calculation can evaluate to `-1` when the composition is shorter than one cycle, which GSAP interprets as an infinite repeat.

**Fix:** Clamp the count at zero: `repeat: Math.max(0, Math.floor(totalDuration / cycleDuration) - 1)`.

### `gsap_repeat_refresh_relative_value`

**Error.** `repeatRefresh: true` combined with a relative value accumulates per iteration; a cold render worker seeking into iteration N never performed the earlier accumulation, so its result differs.

**Fix:** Remove `repeatRefresh: true`, or replace the relative value with absolute endpoints (a `fromTo()`).

### `gsap_repeated_fromto_without_baseline`

**Warning.** Several `tl.fromTo()` calls target the same element with no stable baseline, so the last-authored "from" values become the resting state for any seek before the first tween runs.

**Fix:** Add `immediateRender: false` to every `fromTo` except the earliest-positioned one.

### `gsap_timeline_not_registered`

**Error.** A GSAP timeline is created but never registered on `window.__timelines`, so the runtime never discovers it and the animation does not play in preview or render.

**Fix:** Add `window.__timelines = window.__timelines || {};` and assign the timeline under the composition's `data-composition-id`.

### `gsap_timeline_registered_before_async_build`

**Error.** `window.__timelines` is assigned before the timeline is built inside `document.fonts.ready`, so an empty timeline registers first and can nest empty in sub-compositions.

**Fix:** Move the `window.__timelines[id] = tl` assignment to the end of the async build callback, after the tweens are added.

### `gsap_timeline_return_used_as_tween`

**Error.** Code captures the return of `tl.to()`/`tl.from()`/`tl.set()` expecting the tween, but those methods return the timeline itself — so a later `kill()` or overwrite hits the whole timeline.

**Fix:** Put the group of tweens in a nested timeline (`gsap.timeline()` added with `tl.add(nested, 0)`) and kill that instead.

### `gsap_timeline_set_initial_hide`

**Warning.** The initial hidden state is set with `tl.set(..., 0)` inside the paused timeline; a zero-duration set at position 0 does not render while the playhead sits exactly at 0, so frame 0 shows the un-hidden state.

**Fix:** Use `gsap.set(...)` outside the timeline, or author the hidden state directly in CSS or attributes.

### `gsap_undefined_css_variable`

**Warning.** GSAP animates a `var(--...)` CSS variable that no static CSS or composition-variable declaration defines, so the computed color may become invalid or transparent.

**Fix:** Define the variable in CSS, declare it in `data-composition-variables`, or add a `var(--x, #fff)` fallback.

### `missing_gsap_plugin`

**Error.** A tween uses a plugin-specific property such as `motionPath`, but the plugin is not loaded and registered, so core GSAP ignores the property.

**Fix:** Load the plugin before the animation script and register it with `gsap.registerPlugin(...)`, or express the motion with core `x`/`y` tweens.

### `missing_gsap_script`

**Error.** The composition uses GSAP but no GSAP script is loaded, so the animation will not run.

**Fix:** Add the GSAP `<script>` tag before the animation script.

### `overlapping_gsap_tweens`

**Warning.** Two GSAP tweens overlap on the same target for the same properties, so the later one overwrites the earlier mid-flight.

**Fix:** Shorten the earlier tween, move the later one, or add `overwrite: "auto"`.

### `svg_drawon_css_dasharray_conflict`

**Error.** GSAP writes `strokeDasharray` on an element whose CSS declares a multi-component `stroke-dasharray`; GSAP merges dash lists per component, so the CSS gap survives and the draw-on breaks.

**Fix:** Remove the CSS `stroke-dasharray` (decorative dashes belong on a separate element), or set the full two-component value in GSAP.

### `svg_measure_before_path_d`

**Error / warning.** `getTotalLength()` is called on a path with no static `d` attribute, so it returns 0 and the dash animation dies. Error when no `d` assignment exists anywhere, warning when `d` is only assigned inside a function body.

**Fix:** Assign the path's `d` synchronously at build time (top level, before measuring), or author a static `d` attribute in the HTML.

## Media elements

Source: `packages/lint/src/rules/media.ts`.

### `audio_carve_ungrouped_sources`

**Warning.** A voiceover carve names its member clips directly instead of carving an `<hf-audio-group>`, so the list silently rots when clips change.

**Fix:** Group the voice clips and carve against the group.

### `audio_group_carve_attr`

**Warning.** An `<hf-audio-group>` carries `data-fx-carve` (or a carve-derived chain), but a carve belongs on the bed clip, not the bus.

**Fix:** Remove `data-fx-carve` from the group and carve the bed clip instead.

### `audio_group_no_members`

**Error.** An audio group exists that no clip joins, so its fader, effects and automation are silently dropped.

**Fix:** Add `data-audio-group="<id>"` to the clips the bus is for, or delete the bus.

### `audio_group_timing_attrs`

**Warning.** An `<hf-audio-group>` carries timing attributes (`data-start`, `data-duration`, `data-track-index`). A group has no clip window of its own; its members carry the timing.

**Fix:** Remove the timing attributes from the group and set them on the member clips.

### `audio_volume_double_automation`

**Warning.** An element has both a volume automation lane and a GSAP tween on `volume`; the lane wins and the tween is ignored in preview and render.

**Fix:** Keep one: delete the lane to tween, or drop the tween and shape the level in the lane.

### `audio_volume_tween_overrides_gain`

**Warning.** An element has `data-volume` gain and a GSAP tween on `volume`; tween values are absolute and replace the gain rather than scale it.

**Fix:** Write tween targets as absolute gains that already include the gain, or reset `data-volume` to 1 and let the tween carry the level.

### `base64_media_prohibited`

**Error.** Inline base64 audio or video is present. Base64 media bloats file size and breaks rendering.

**Fix:** Use a relative path or HTTPS URL for the `src` — never embed media as base64.

### `color_grading_invalid_json`

**Error.** `data-color-grading` on a `<video>` or `<img>` starts with `{` or `[` but is not valid JSON, so the grade does not render.

**Fix:** Use valid JSON, for example `data-color-grading='{"preset":"skin-soft","intensity":0.6,"adjust":{"highlights":-0.08}}'` — single-quote the attribute, double-quote the JSON.

### `color_grading_invalid_structure`

**Error.** `data-color-grading` parses as JSON but breaks the grading contract — an unsupported key, a value of the wrong type, or a number out of range. The runtime silently ignores unknown keys, so a control can look applied in the HTML yet never render. One finding is reported per problem, naming the offending path.

**Fix:** Correct or remove the value the finding names, following the [Color grading](/reference/color-grading) reference.

### `color_grading_non_media`

**Error.** `data-color-grading` is set on an element other than `<video>` or `<img>`, such as a wrapper or an element with a CSS background. The shader runtime only grades real `<video>` and `<img>` elements, so the attribute has no effect.

**Fix:** Move the attribute to the `<video>` or `<img>` element itself.

### `duplicate_media_discovery_risk`

**Warning.** Several matching media entries share the same source, start and duration, so the compiler can discover the same media twice.

**Fix:** Remove the duplicated media nodes so discovery sees one.

### `duplicate_media_id`

**Error.** A media `id` is defined on multiple elements, so preview and the producer can build different media graphs.

**Fix:** Give each media element a unique `id`.

### `imperative_media_control`

**Error.** An inline script controls managed media imperatively (`play`, `pause`, `currentTime`, ...). HyperFrames must own playback to keep preview, timeline and renders deterministic.

**Fix:** Express timing with `data-start`/`data-duration` and media offsets such as `data-media-start` instead.

### `media_crossorigin_breaks_preview`

**Error.** The element has `crossorigin`, which forces a CORS-checked fetch; if the host omits CORS headers, the load fails silently in Studio preview (blank media) while renders still work.

**Fix:** Remove `crossorigin` unless you read the media back through canvas, WebGL or WebAudio from a host known to send CORS headers.

### `media_missing_data_start`

**Error.** A media element has `src` but no `data-start`, so HyperFrames cannot own its playback and preview and render can diverge.

**Fix:** Add `data-start="0"` (or the intended start) and `data-duration` if the clip should stop before the source ends.

### `media_missing_id`

**Error.** A timed media element has no `id`, so the renderer cannot discover it — the audio goes silent or the video freezes.

**Fix:** Add a unique `id` attribute to the element.

### `media_missing_src`

**Error.** A timed media element has no `src` (on the element or a `<source>` child), so the renderer cannot load it.

**Fix:** Add `src` to the element, or a `<source src="...">` child.

### `media_preload_none`

**Warning.** The element has `preload="none"`, which prevents the renderer from loading the media (the compiler strips it for renders, but preview can misbehave).

**Fix:** Remove `preload="none"` or change it to `preload="auto"`.

### `media_runtime_src_mutation`

**Warning.** An inline script mutates the `src` of existing managed media, which races media discovery and extraction.

**Fix:** Author the final static `src`, or bind `data-var-src` to a declared variable so the source is applied before discovery.

### `media_src_kind_mismatch`

**Error.** A media URL's kind does not match its element — for example a still image on `<video>` or a video URL on `<img>`, so the element renders nothing.

**Fix:** Match the element to the media: `<img>` for stills, `<video>` for video URLs, `<audio>` for sound.

### `media_variable_src_no_fallback`

**Warning.** The element relies on `data-var-src` with no fallback `src`, so renders without that variable value cannot load the media, and audio extraction reads the authored `src`.

**Fix:** Add a fallback `src` the composition can render with when the variable is not provided.

### `nested_media_start_basis_ambiguous`

**Warning.** A nested media element's `data-start` is ambiguous: composition-local or a legacy root-global timestamp, and the two read as different frames.

**Fix:** Keep `data-start` composition-local, or mark the legacy basis with `data-hf-media-start-basis="global"`; otherwise convert to local time.

### `placeholder_media_url`

**Error.** The element uses a placeholder media URL that will 404 at render time.

**Fix:** Replace it with a real, reachable media URL.

### `self_closing_media_tag`

**Error.** A self-closing media tag (`<video ... />`) is invalid HTML: the browser leaves the tag open and swallows all subsequent elements as invisible fallback content, making the composition invisible.

**Fix:** Write explicit closing tags: `<video ...></video>`. Media elements must never be self-closed.

### `speed_ramp_on_non_media`

**Warning.** A speed-ramp lane sits on a non-media element; only `<video>` and `<audio>` clips can be retimed, so the lane does nothing.

**Fix:** Move the rate lane onto the video or audio clip, or retime an animation with a GSAP timeline.

### `video_audio_double_source`

**Error.** An `<audio>` element and a `<video>` element point at the same source at the same time; the unmuted video already carries the sound, so playback echoes.

**Fix:** Let the video carry its own sound (recommended), or mute the video and keep the separate `<audio>` when picture and sound must be cut independently.

### `video_missing_muted`

**Error.** A timed `<video>` declares neither `muted` nor `data-has-audio="true"`, so the linter cannot tell silent footage from sound-carrying footage.

**Fix:** Add `data-has-audio="true"` when the file has sound, or `muted` when it is silent.

### `video_muted_with_declared_audio`

**Error.** A `<video>` declares `data-has-audio="true"` but also carries `muted`, so Studio preview silences the audio the declaration promises.

**Fix:** Remove `muted` if the video should be audible, or drop `data-has-audio` and use `data-volume="0"` for silent visual video.

### `video_nested_in_timed_element`

**Error.** A `<video>` with `data-start` is nested inside another timed element; the frame extractor resolves the video's start without the wrapper's offset while visibility uses the wrapper's window, so the clip shows wrong frames and disappears partway.

**Fix:** Time the wrapper or the video, never both: remove `data-start` from the wrapper, or move the `<video>` up to be a direct child of the stage.

## Captions

Source: `packages/lint/src/rules/captions.ts`.

### `caption_container_relative_position`

**Error.** A caption selector uses `position: relative`, which causes overflow and breaks caption stacking.

**Fix:** Use `position: absolute` for all caption elements.

### `caption_exit_missing_hard_kill`

**Error.** Caption exit animations (`tl.to` with `opacity: 0`) have no hard `tl.set` kill, so conflicting karaoke tweens can leave captions stuck on screen.

**Fix:** Add `tl.set(groupEl, { opacity: 0, visibility: "hidden" }, group.end)` after every exit tween as a deterministic kill.

### `caption_fittext_scale_mismatch`

**Warning.** `fitTextFontSize` uses a `maxWidth` that leaves no headroom for emphasis words scaled above 1x, so the effective width can overflow the composition.

**Fix:** Reduce `maxWidth` to leave headroom for the scale factor.

### `caption_overflow_clips_scaled_words`

**Error.** A caption container has `overflow: hidden` while GSAP scales caption words above 1x, so scaled emphasis words and their glow are clipped.

**Fix:** Use `overflow: visible` on caption containers and prevent overflow with `fitTextFontSize` and a reduced `maxWidth` instead.

### `caption_text_overflow_risk`

**Warning.** A caption selector has `white-space: nowrap` but no `max-width`, so long phrases clip off-screen.

**Fix:** Add `max-width: 1600px` (landscape) or `900px` (portrait), keeping overflow visible.

### `caption_textshadow_on_group_container`

**Warning.** `textShadow` is tweened on a caption group container; with semi-transparent children the glow renders as a visible rectangle behind the whole group.

**Fix:** Apply `textShadow` to individual active word elements and use scale on the group for pulsing.

### `caption_transcript_not_inline`

**Error.** A captions composition loads its transcript with `fetch()`, but the Studio caption editor needs an inline `var TRANSCRIPT = [...]` array to detect and edit captions.

**Fix:** Embed the transcript inline as `var TRANSCRIPT = [...]` with JSON-quoted keys.

## Fonts

Source: `packages/lint/src/rules/fonts.ts`.

### `font_family_without_font_face`

**Error.** A `@font-face`-undeclared font family is used, so the renderer cannot supply it and text falls back to a default face.

**Fix:** Declare each family with `@font-face` pointing at the captured `.woff2` files, or bundle the font under `capture/assets/fonts/`.

### `system_font_will_alias`

**Warning.** A system font family is used without a matching `@font-face`; in distributed rendering system-font capture is disabled, so the font is substituted at render time and text can reflow. Only reported when the linter runs with the `distributed: true` option (linting for distributed or Lambda renders); a local render captures the system font as designed, so it is not flagged there.

**Fix:** Bundle the font with `@font-face` and a project-local file instead of relying on OS fonts.

## Textures

Source: `packages/lint/src/rules/textures.ts`.

### `texture_class_missing_base`

**Warning.** A texture material class is used without the base `hf-texture-text` class, so the texture pipeline does not engage.

**Fix:** Add the base class alongside the material class, e.g. `class="hf-texture-text hf-texture-lava"`.

### `texture_class_unknown`

**Error.** A texture material class is not defined by any local CSS, so the text renders untextured.

**Fix:** Paste the Texture Mask Text component's `<style>` block into the composition, or fix the class typo.

### `texture_drop_shadow_on_text`

**Warning.** A drop shadow is applied directly to textured text, which shadows the mask rather than the glyphs.

**Fix:** Wrap the textured text and apply `filter: drop-shadow(...)` to the wrapper, not the `hf-texture-text` element.

### `texture_text_missing_mask`

**Warning.** `hf-texture-text` is used without a texture material class or a custom `mask-image`, so there is nothing to reveal.

**Fix:** Add a material class such as `hf-texture-lava`, or set `mask-image` and `-webkit-mask-image` on the element.

## Slideshows

Source: `packages/lint/src/rules/slideshow.ts`.

### `slideshow_invalid`

**Error.** A slideshow island contains invalid JSON or a structure that does not match the manifest schema.

**Fix:** Make the `<script type="application/hyperframes-slideshow+json">` block valid JSON matching the SlideshowManifest schema.

### `slideshow_unresolved_ref`

**Error.** The slideshow manifest references a `sceneId` with no matching scene element, or a scene with no explicit window.

**Fix:** Make every `sceneId` match a scene element's `data-composition-id`, or provide explicit `startTime`/`endTime`.

## Adapters: Lottie and Three.js

Source: `packages/lint/src/rules/adapters.ts`.

### `missing_lottie_script`

**Error.** The composition uses Lottie but no Lottie script is loaded, so the animation will not render.

**Fix:** Add the `lottie-web` `<script>` tag before the Lottie code.

### `missing_three_script`

**Error.** The composition uses Three.js but no Three.js script is loaded, so the 3D scene will not render.

**Fix:** Add the Three.js `<script>` tag before the Three.js code.

## Linked clips

Source: `packages/lint/src/rules/linkedClips.ts`.

### `linked_clip_orphan`

**Warning.** A clip carries `data-link` whose partner clip no longer exists, so it edits as a group of one.

**Fix:** Remove the `data-link` attribute, or restore the partner clip.

### `linked_clips_out_of_sync`

**Warning.** Clips sharing a `data-link` differ in their timing attributes, but linked clips are edited as one.

**Fix:** Give every member the same `data-start`, `data-duration`, `data-media-start` and `data-playback-rate`, or remove `data-link` from all of them to unlink.

## Project-level checks

Source: the project lint pass in `packages/lint/src/project.ts`, `packages/lint/src/hyperframeLinter.ts` and `packages/lint/src/hevcPreviewLint.ts`.

### `multiple_root_compositions`

**Error.** Several root-level HTML files carry `data-composition-id`, so the runtime may discover multiple entry points and duplicate audio playback.

**Fix:** Keep exactly one root `index.html` with `data-composition-id`; remove or rename the extra files.

### `blank_root_with_standalone_composition`

**Error.** The default `index.html` has no renderable content while another file holds a standalone timed composition, so default commands open an empty video.

**Fix:** Move the composition into `index.html`, or mount it with `data-composition-src`; if the separate file is intentional, render it explicitly.

### `missing_or_empty_sub_composition`

**Error.** A `data-composition-src` points at a folder, an empty file, or a missing file, so the mount is unusable and the render pre-flight rejects it.

**Fix:** Point `data-composition-src` at the HTML file (such as `<folder>/index.html`) and make sure it renders content.

### `missing_local_asset`

**Error.** An element references local files that do not exist in the project, so the renderer silently skips them and the video ships with missing visuals.

**Fix:** Add the file to the project directory, or update the `src` to an existing file. A common cause is renamed captured assets.

### `audio_file_without_element`

**Warning.** The project contains audio files but no `<audio>` element in any composition, so the rendered video will be silent.

**Fix:** Add an `<audio>` element referencing the file.

### `audio_src_not_found`

**Error.** An `<audio>` element references files that are not in the project, so the rendered video will be silent.

**Fix:** Add the missing file(s) to the project directory, or update the `src` to point at existing files.

### `duplicate_audio_track`

**Warning.** Two audible elements overlap on the same track index, so their audio doubles up.

**Fix:** Use non-overlapping time windows or different track indices.

### `texture_mask_asset_not_found`

**Error.** A CSS `mask-image` references files that are not in the project, so the texture mask fails to load.

**Fix:** Add the missing mask file(s) to the project, or update the `mask-image` URL.

### `inaccessible_media_url`

**Error.** A remote media URL did not answer a `HEAD` request with a 2xx status (or the request failed or timed out), so the media cannot load. Reported only by the async `lintMediaUrls()` export of `@hyperframes/lint`; `hyperframes lint` and `hyperframes check` do not run it.

**Fix:** Replace it with a valid, reachable media URL.

### `hevc_preview_codec`

**Info.** A video file uses the HEVC/H.265 codec. Renders are unaffected (FFmpeg pre-decodes), and preview automatically falls back to a cached H.264 proxy when the browser cannot decode HEVC.

**Fix:** Nothing required. If preview playback still fails, run `hyperframes doctor` and check that media auto-proxying is enabled.

### `video_media_start_at_or_past_eof`

**Warning.** A video's `data-media-start` sits at or past the source's playable duration, so the video holds its final frame for the whole slot.

**Fix:** Trim `data-media-start` below the source duration if the final-frame hold is unintended.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.