Skip to content

(studio) timeline drag rewrites HTML with inverted inline z-index on every clip, overrides CSS z-index documented as layering source of truth #958

@marcelo-franco

Description

@marcelo-franco

Describe the bug

When dragging a clip horizontally in the Studio's timeline editor, the Studio rewrites index.html on disk and:

  1. Injects inline style="z-index: N" on every clip in the file — not only on the clip that was dragged. The injected values are derived from data-track-index in inverse order (track 0 receives the highest z-index, the last track receives the lowest). Inline-style specificity overrides any CSS z-index the author defined.

  2. Produces malformed self-closing tags when adding the inline attribute to void elements. Example output:

<img
  id="gif-img"
  ...
  alt="rotating earth gif"
/ style="z-index: 7">

The / of the self-closing img is no longer adjacent to >. HTML5 parsers tolerate this, so rendering still works, but it's an authoring artifact that surprises diff readers and any downstream tooling that parses the file with a strict parser.

The first issue contradicts the official skill documentation, which explicitly states:

data-track-index does not affect visual layering — use CSS z-index.

Steps to reproduce

  1. npx hyperframes init repro --example blank --non-interactive
  2. Edit index.html to add a background video + an overlay with explicit CSS z-index whose visual order does not match data-track-index order:
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body { margin: 0; width: 1920px; height: 1080px; overflow: hidden; background: #000; }
  #bg-video  { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0; }
  #title     { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
               font: 700 120px sans-serif; color: #fff; z-index: 20; }
</style>

<div id="root" data-composition-id="main" data-start="0" data-duration="10"
     data-width="1920" data-height="1080">
  <video id="bg-video" data-start="0" data-track-index="0" src="some-bg.mp4" muted playsinline></video>
  <div   id="title" class="clip" data-start="0" data-duration="10" data-track-index="1">TITLE</div>
</div>
  1. npx hyperframes preview
  2. In the Studio, drag the video clip's block horizontally on the timeline (any small distance that commits — typically more than ~2 seconds of timeline).
  3. Re-read index.html from disk.

Expected behavior

  • Only data-start (and possibly data-duration) of the dragged clip is modified.
  • The author's CSS z-index remains the source of truth for visual layering, as documented.
  • Other clips are left untouched.
  • Void elements stay well-formed.

Actual behavior

Every clip in the file is rewritten with an inline style="z-index: N" attribute. Example diff:

- <video id="bg-video" data-start="0" data-track-index="0" src="some-bg.mp4" muted playsinline></video>
+ <video id="bg-video" data-start="0" data-track-index="0" src="some-bg.mp4" muted playsinline style="z-index: 9"></video>

- <div id="title" class="clip" data-start="0" data-duration="10" data-track-index="1">TITLE</div>
+ <div id="title" class="clip" data-start="0" data-duration="10" data-track-index="1" style="z-index: 8">TITLE</div>

Result: inline z-index: 9 on the video (CSS z-index was 0) now sits on top of the title's inline z-index: 8 (CSS z-index was 20). The intended layering is silently inverted. In a composition with a full-bleed background video, all overlays become permanently hidden behind it even though the original CSS placed them on top.

When the dragged element is an <img> (a void element), the inline attribute is injected with a misplaced self-closing slash:

<img
  id="gif-img"
  class="clip"
  data-start="1"
  data-duration="13"
  data-track-index="2"
  src="assets/earth.gif"
  alt="rotating earth gif"
/ style="z-index: 7">

Empirical observations during the drag gesture

Verified via Playwright with MutationObserver installed inside the composition iframe before the drag:

Phase Mutations on iframe .clip elements Mutations on parent Studio UI
During the drag (~2.4s, 30 mouse-move events) 0 38 (~15 mut/s on the drag-preview ghost element)
On mouseup (commit) iframe reloads with new file contents (observer loses references)

So the bug isn't a per-frame mutation issue during the drag itself — it's a serialization issue when the Studio commits the edit to disk and reinjects layering metadata that wasn't there before.

Why this matters

  • Composition author's intent (CSS z-index) is silently overridden by Studio's metadata derivation.
  • Documented contract (data-track-index does not affect layering) is broken by the Studio's own behavior.
  • The corruption is persistent in the file — surviving page reload, restart, and shared via VCS.
  • Any composition using CSS z-index for non-default stacking will be broken the first time someone drags a clip in the Studio.

Environment

✓ Version          0.6.25 (latest)
  Node.js          v22+
  OS               Windows 11 (10.0.26200)
  Shell            PowerShell

Suggested fix direction

  1. Preserve the author's CSS z-index and stop auto-injecting inline z-index when committing a timeline edit. If the Studio needs an internal ordering for the layers inspector, compute it without mutating the source HTML.
  2. If automatic z-index injection is intentional, at minimum:
    • Use a consistent and documented ordering (matching DOM order, or the existing CSS z-index if defined).
    • Honor existing inline/CSS z-index instead of overwriting it.
    • Add a project-level opt-out flag.
  3. Fix the HTML serializer to handle void elements correctly when injecting attributes: produce <img ... style="..." /> (or <img ... style="...">), not <img ... / style="...">.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions