HTML Reel, a skill for Claude Code, Codex and Cursor

A skill for Claude Code, Codex and Cursor: turn a tweet, a video, a YouTube video or a landing page into a 9:16 reel that says what it says and looks like it, its scenes HTML drawn from one clock in the source’s fonts and colours, cut to every word of your voice, and retimed to your own takes if you record them.

Install it

Claude Code:

curl -fsSL https://fil.design/skills/html-reel.zip -o /tmp/html-reel.zip && unzip -o /tmp/html-reel.zip -d ~/.claude/skills

Codex:

curl -fsSL https://fil.design/skills/html-reel.zip -o /tmp/html-reel.zip && unzip -o /tmp/html-reel.zip -d ~/.codex/skills

GitHub:

npx degit MengTo/Skills/agent-skills/media/html-reel ~/.claude/skills/html-reel

Or make one in Fil with its workflow: https://fil.design/workflows/html-reel.

Or tell your agent:

Install the HTML Reel skill from https://fil.design/skills/html-reel: unzip https://fil.design/skills/html-reel.zip into your skills folder (~/.claude/skills for Claude Code, ~/.codex/skills for Codex), then read its SKILL.md.

The skill’s folder: https://fil.design/skills/html-reel.zip (zip), GitHub, and each file:

SKILL.md

HTML scenes · DreamCut · Fil's video editor over MCP · ffmpeg. A scene engine without a framework is in scripts/reel-clock.js.

A reel is a voice with pictures cut to it. This skill makes one from a source someone already has, a tweet, a landing page, a YouTube video or a video file, so it says what that source says and looks like it: its words become the script, its media the picture, its fonts and colours the titles. Every picture follows the words: a title lands on its word, the demo cuts on the line that names it, a sound effect hits on the reveal. The creator is in it from the first draft, drawn in ink where their camera goes, so the reel is complete before they record a word; their takes replace the drawing, and because the scenes are HTML drawn from one clock, everything is retimed to their real delivery without animating anything again.

Boundary. Use html-demo for a live app window on a web page (no video file). Use browser-video-recording for a polished screen recording on its own. Use website-to-motion-design to turn a page into a fixed-length motion piece with no voice. Use this skill when the deliverable is a vertical video cut to a voice, from a source.

Provenance. Extracted from Meng To's launch reel for HTML Demo (fil.design/skills/html-demo), October 2026: a 24.7 s reference reel's eight layouts studied, a scratch voice in his cloned ElevenLabs voice, four DreamCut recordings of the skill page, an HTML hook and tail rendered frame by frame, three drafts in Fil's video editor, two blind judges a round, then his two phone takes cut into one 33.7 s reel on his own word timings. Then made general: each of three of his posts (MiniMax landing pages, a reverse-engineered smart home, the Anatomy skill) turned into a reel in its own look, first faceless, then again with his ink placeholder in the camera layouts, cinematic type and captions, previewed locally before Fil. The files and numbers below are from those runs.

The mechanism

Everything is placed on word times, and everything drawn is a pure function of t. Each HTML scene sets every element from t, the seconds into the scene, through window.__frame(t), at a fixed 540 × 960 stage rendered at 2× (1080 × 1920). A renderer calls it once a frame and screenshots; nothing reads a clock. So when the real take arrives, word anchors give a piecewise-linear map from the real seconds τ to the scratch seconds t, and the scene renders again at t = W(τ): the same animation, on the new delivery, with no judder.

Take that away and the reel can't follow a person. A scene animated with CSS timelines, GSAP timelines or timers plays at its own pace; to fit a new take it has to be re-animated by hand, line by line, and the cuts drift a few frames each time. A screen recording can't be retimed either, so it's cut at its events and each piece gets its own speed.

// hook.js, on reel-clock.js: times are the scratch voice's words.
const T = { look: 0.27, recording: 1.1, zoom: 3.25, sharp: 4.54, html: 7.22 };
HTMLReel.scene({
  dur: 8.6,
  anchors: ANCHORS,                       // [[τ, t], …] once the real take exists
  draw(t, $) {
    $('#win').style.transform = `scale(${1 + 2.8 * ease(t, T.zoom, T.zoom + 1.2)})`;
    $('#w-recording').style.transform = `translateY(${(1 - word(t, T.recording, 2.2)) * 135}%)`;
    $('#tags').style.opacity = ease(t, T.html - 0.3, T.html);
  },
});

scripts/render-frames.mjs renders any such page to an MP4 (and checks the safe area every frame); Fil's video editor plays the same page as an HTML scene, stepping its clock frame by frame; a browser plays it live with ?controls.

Workflow

  1. Read the source (scripts/read-source.mjs <tweet | page | YouTube | file> --out source --follow, references/source.md). Finish Source.md: its words word for word, the facts the reel may say, the one idea, the hook, whose work it is, the ask. Finish Look.md from what was measured and the frames: the shape, ground, ink, accent, and type as three roles (setup, payoff, labels), background, motifs, motion.
  2. Pick the structure, the shape and the layouts (references/structures.md): a promo (references/formula.md, studied from a reference reel), a showcase, "found this", an explainer, before and after; 9:16, 4:5, 1:1 or 16:9; and for each beat its layout, the creator's face growing as the ask grows (none in the hook, a bubble, half the frame, full for the ask). Faceless only when the creator won't be on camera. A reference reel the creator likes gives structure and timing, never its look.
    • Draw the creator as their placeholder: from a photo of them, a clean black ink line drawing on warm cream paper, head and shoulders, looking into the lens, upright 9:16, labelled CAMERA · PLACEHOLDER (in Fil, an image node editing their photo with Nano Banana). It stands in every camera layout until a take replaces it.
  3. Write the script in the creator's voice from Source.md, one line per beat: their own sentences, shortened; only the facts Source.md lists; other people credited by name. Size it to the voice's pace, not a reader's: a cloned voice reads about 2.2–2.4 words a second with its sentence pauses (a person on camera about 2.6), so 30 s holds about 65–70 words. Write for the ear: numbers and names as they're said ("five point oh", "Rea"), capitals only on screen. Mark each line with its layout, what the picture shows and the word each move lands on.
  4. Make the voice with word timestamps (scripts/vo-words.py: ElevenLabs with-timestamps, the creator's cloned voice, the key from the environment and never printed; it saves the whole reply, so the words can be made again for free, and prints the pace). For a faceless reel it's the final voice; with takes coming, a scratch voice. Then:
    • check what it said before building on it: Fil's captions on it, or a local whisper, word by word against the script (names and numbers above all);
    • too long by a second or two: scripts/tighten-voice.py cuts the middle of every long pause and moves the words with them, instead of paying for another take;
    • name every word (scripts/named-times.py: T.zoom, T.skill2, T.htmlEnd), and cut everything to those names from here on. Ask before any spend over $1.
  5. Build the picture in the source's look, every scene on scripts/reel-clock.js with Look.md's variables in its :root and HTMLReel.scene({ shape }):
    • a demo video framed and cut to words: its pieces (from, to, and the word each lasts until) into scripts/frame-video.mjs, several videos in one body (a post's and the post it quotes), a full-frame card spliced in as a piece, a title layer drawn from t over it (scripts/render-frames.mjs --alpha), checked as stills over the body first (--test … --under body.mp4);
    • full scenes for the hook, a title card or the tail, rendered with scripts/render-frames.mjs; a hook that shows the source's video inside its own frame draws the video's frames as pictures (extracted once at 60 fps, decoded in setup, draw setting the one for t and awaiting it), so it retimes like the rest;
    • a product the source doesn't show, recorded with DreamCut (scripts/record-window.mjs, scripts/flash-crop.mjs), the pointer's moves timed to words. Everything read stays inside the safe area (y 280–1640 of 1920 on 9:16).
  6. Preview it locally (scripts/preview-reel.mjs plan.json): the picture, the placeholder in each camera layout at the editor's own set values, cinematic captions word by word, the voice and the sound effects, at −14 LUFS, with a contact sheet. Judge the layouts with words on them here, where a change costs a minute; an export costs a window and a wait.
  7. Assemble in Fil's video editor over MCP (references/fil-editor.md): the picture as the story; the placeholder over it as overlays in each camera layout (full, the bubble with its shrink, a half), so a take can replace it later keeping every crop, place and keyframe; the voice on its own lane; sound effects at volume 0.1–0.2; cinematic captions on the seam or under the picture, never on the face; the beats as chapters. Export, download, look at the frames.
  8. Score it to 8/10 with blind judges (references/judging.md): two absolute judges on frames and a contact sheet, the lower score counts, plus a paired judge on before/after. Fix what both name; stop when two rounds come back flat.
  9. The creator records: put the script in a canvas document as a teleprompter (headings and [directions] show but aren't read), add Record on your phone, and hand them the QR code as a picture. Their takes land on the canvas, the voice cleaned. replace each placeholder with its take, then retime everything to their words (references/retime.md):
    • join the takes (scripts/join-takes.py): pauses over ~0.45 s trimmed, 15 ms fades, each join on a layout change;
    • word times from Fil's captions (look.max_words: 1, then get_video captions list, scripts/fil-words.py);
    • align scratch and real words into anchors (scripts/timemap.py), render the scenes again on them, split and speed the video pieces;
    • lay in the camera: the bubble (set.corner_radius in the editor, or scripts/bubble.mjs) and the split (scripts/split-flip.mjs);
    • captions again from the real voice, chapters, export, and scripts/verify-export.mjs.

Rules, each with the failure it prevents

Numbers from the three posts (drafts with the placeholder)

whatvalue
length29.5–33.9 s, 1080 × 1920, 30 fps, −14.4 to −14.7 LUFS
voiceone ElevenLabs take each, about 400 characters; 2.2–2.4 words a second; one tightened from 31.8 s to 29.5 s
camerano face in the hook (3–6 s); full frame 0.9–2.7 s; a bubble 3–7.5 s; half the frame 9–22 s; the top half for the ask, 3.7–4.1 s
pieces7–19 a reel, speeds 0.26 (a still window) to 2.02, a crop on the detail every few pieces
titlessetup 52 px, payoff 100–104 px on the 540 stage; the ask's keyword fitted to 492 px (seven letters: 96–112 px)
captionsInter Tight 78 px, 800, ink and accent from Look.md, 5 px outline, on the seam; 20–29 lines, 3–5 removed under big type or a full-frame face
sound effects4 each, volume 0.10–0.16, on reveals, a card, the keyword

Numbers that shipped (the HTML Demo reel)

whatvalue
length33.7 s, 1080 × 1920, 30 fps, −14 LUFS
sectionsHook 0–4.74 · Twist –7.77 · Proof –11.70 · Demo –16.33 · Benefit –22.04 · Offer –25.65 · Value stack –31.06 · CTA –33.70
paceabout 2.6 words a second; scratch voice 37.8 s, real takes cut to 33.4 s
takestwo upright phone takes; pauses over 0.45 s trimmed; 15 ms fades; 0.8 s beat after "it's HTML"
stage540 × 960 CSS at 2×; safe area y 280–1640 of 1920
titlesInstrument Serif 52 px, then Anton 100 px uppercase with a 0.17 em accent dot; masks padded 0.14 em / 0.2 em
bubble268 × 375 at x 721, y 1072, 30 px corners, a 2 px edge at 55% white; shrinks from full frame over 0.35 s
splita hard edge at y 960; the camera's middle 1080 × 960, zooming 0.86 → 1 over 0.35 s; flips to the top half over 0.32 s, cubic in-out
captionsInter Tight 78 px, weight 800, white, the spoken word in #efb36c, 5 px dark outline, middle, up to 3 words
sound effectsvolume 0.1–0.2: soft whoosh on cuts, click on presses, pop on a card, typing under typing, impact on the CTA
anchors24 in the hook, monotone, 4 manual pairs
screen piecesspeeds 1.13–2.07 to land the scrub and the step jumps on their words
judgestwo blind absolute judges (lower counts) + one paired judge; the hook went 6.5 → 6.0 → 7.3 → 7.0 and stopped flat

Verify

Files