Motion graphics in 80 lines
A complete title sequence — bouncy text, parallax backdrop, signal-color accent, cinematic ease — written in 80 lines of plain HTML. No framework. No tooling beyond the browser.
Show, don't tell. So here is the file. Eighty lines, complete, no dependencies, deterministic when run through hyperframes render. It produces a five-second title sequence with bouncy character animation, a parallax backdrop, a signal-color accent line, and a cinematic ease-out. I will walk through every decision after the listing, but I want you to see the whole thing first, because the surprise is that there is no surprise.
<!DOCTYPE html>
<html>
<head>
<style>
:root {
--cream: #f6f5f1;
--ink: #0a0a0a;
--signal: #ff3b1f;
--ease: cubic-bezier(.16, 1, .3, 1);
}
html, body { margin: 0; height: 100vh; overflow: hidden;
background: var(--cream); color: var(--ink);
font-family: "Newsreader", Georgia, serif; }
.stage { position: relative; height: 100vh;
display: grid; place-items: center; }
.backdrop { position: absolute; inset: 0;
background: radial-gradient(ellipse at 30% 40%,
color-mix(in oklab, var(--signal) 14%, transparent),
transparent 60%);
animation: drift 7s var(--ease) both;
animation-play-state: paused;
animation-delay: calc(var(--hf-time, 0s) * -1); }
@keyframes drift {
from { transform: scale(1.1) translateX(-3%); opacity: 0; }
20% { opacity: 1; }
to { transform: scale(1.0) translateX(3%); opacity: 1; }
}
h1 { position: relative; z-index: 1;
font-size: clamp(56px, 9vw, 140px);
font-weight: 500; letter-spacing: -0.025em;
line-height: 0.95; margin: 0; max-width: 12ch; text-align: center; }
.ch { display: inline-block;
animation: rise 800ms var(--ease) both;
animation-play-state: paused;
animation-delay: calc((var(--hf-time, 0s) * -1) + var(--d, 0s)); }
@keyframes rise {
from { opacity: 0; transform: translateY(40px) rotateX(70deg); }
to { opacity: 1; transform: translateY(0) rotateX(0); }
}
em { color: var(--signal); font-style: italic; }
.rule { position: absolute; bottom: 16vh; left: 50%;
width: 0; height: 2px; background: var(--signal);
transform: translateX(-50%);
animation: stretch 1.2s var(--ease) 2.4s both;
animation-play-state: paused;
animation-delay: calc((var(--hf-time, 0s) * -1) + 2.4s); }
@keyframes stretch { to { width: 22vw; } }
.caption { position: absolute; bottom: 10vh; left: 50%;
transform: translateX(-50%);
font-family: ui-monospace, monospace;
font-size: 12px; letter-spacing: 0.22em;
text-transform: uppercase;
color: color-mix(in oklab, var(--ink) 60%, transparent);
opacity: 0;
animation: fade 600ms var(--ease) 3s both;
animation-play-state: paused;
animation-delay: calc((var(--hf-time, 0s) * -1) + 3s); }
@keyframes fade { to { opacity: 1; } }
</style>
</head>
<body data-duration="5">
<div class="stage">
<div class="backdrop"></div>
<h1 id="title">A film for <em>frame zero</em>.</h1>
<div class="rule"></div>
<div class="caption">a hyperframes original</div>
</div>
<script>
const t = document.getElementById("title");
t.innerHTML = t.innerHTML.replace(/(\S)/g, (m, c, i) =>
`<span class="ch" style="--d:${(i * 35) + 200}ms">${c}</span>`)
.replace(/<span class="ch"[^>]*> <\/span>/g, " ");
</script>
</body>
</html>That is the whole composition. Save it as title.html, run npx hyperframes render title.html --duration 5, and you have an MP4. Now let us go through the design choices.
The CSS variable trick
The single most important pattern in this file is the use of --hf-time. HyperFrames sets this variable on :root for every frame. CSS animations are paused (animation-play-state: paused) and their animation-delay is computed from --hf-time. The result is that every animation is driven by the engine's clock, not the browser's wall clock. This is what makes the render deterministic.
Note the formula: animation-delay: calc((var(--hf-time, 0s) * -1) + var(--start, 0s)). The negative time scrubs the animation forward; the start offset positions when it begins. When --hf-time is 0s, the animation is at its start. When --hf-time is 2.4s and the start is 2.4s, the animation has just begun. This idiom is in every HyperFrames composition I write.
The character split
The script at the bottom is the only JavaScript in the file, and it runs exactly once at page load. It walks the title text, wraps every non-space character in a span, and gives each span a staggered --d. The CSS animation rule reads --d and offsets the rise by it.
This pattern — split characters, stagger animation-delay — is the most reused motion pattern in editorial design. You see it on every well-designed news graphic. The reason it works is that the human eye reads characters left-to-right at roughly 35-50ms per character, and when the animation matches that cadence, the title appears to land into place rather than fade in as a block.
I have tried this with libraries — SplitText from GreenSock, splitting in JavaScript at runtime, splitting with CSS pseudo-elements. The plain HTML approach beats all of them for clarity and for render speed. The DOM is built once, the animation is declarative, the engine seeks into it.
The easing curve, specifically
cubic-bezier(.16, 1, .3, 1) is the most important number in this composition. It is the easing curve I use as my default for editorial motion. It comes from a long line of "cinematic" eases — Apple's Big Sur curve, the Material 3 "emphasized" easing, the unofficial "EaseOutExpo" that motion designers have shared on Twitter for a decade.
The key feature of the curve is that it spends most of its travel in the first 30% of the duration. The element moves fast, then slows, then settles. Settling is what makes motion feel intentional. Linear motion feels mechanical; quadratic motion feels generic; this curve feels like something a hand placed there.
I have an entire post coming on easing that looks like money, but the short version is: every editorial motion designer has three or four curves in their muscle memory, and changing one of them changes the entire personality of a video. This curve is one of mine. It is not the only correct answer. It is a correct answer.
The backdrop
The backdrop is a single radial-gradient, color-mixed at 14% opacity with the signal color. The reason it works is that it is subtle. The role of the backdrop is not to be seen. It is to give the eye somewhere soft to land before the title arrives, and to add chromatic warmth to a frame that is otherwise pure cream and ink.
The drift animation moves the gradient slightly over seven seconds. The displacement is 6% of the viewport — large enough to register as motion, small enough that you do not catch it consciously. This is the difference between motion graphics that feel alive and motion graphics that feel static. Backgrounds in the best editorial work are always moving, but you have to look to see it.
The rule and the caption
The rule (the 22vw signal-colored line) is the second beat of the composition. It enters at 2.4 seconds — after the title has settled but before the eye gets bored. The width stretches from 0 to 22vw over 1.2 seconds with the same cinematic ease. It is a single <div> and a single keyframe.
The caption ("a hyperframes original") arrives at 3 seconds. It is monospaced, all-caps, letter-spaced wide, and dimmed to 60% ink. The combination of the rule and the caption is the editorial equivalent of a director's credit on a film — small, confident, in service to the title above. The font shift from serif to mono is the visual equivalent of a different voice.
I want to point out that nothing in this composition is technically impressive. There is no shader. There is no Lottie. There is no WebGL. There is not even a JavaScript animation library. The whole file is HTML, CSS, and one small script. The motion designers I respect most can produce work like this in twenty minutes; the rest of us can produce it in an hour with a reference.
What 80 lines buys you
I want to land the post on this point. Eighty lines of HTML is not a small amount of code, but it is a small amount of artifact. The compositions I see motion designers ship in After Effects are typically project files that are megabytes large, with sixty layers, two dozen pre-comps, and a maze of expressions. The output is gorgeous. The artifact is unreviewable.
This 80-line file, by contrast, fits in a pull request. A reviewer can read it. An agent can edit it. (See the After Effects comparison for the longer version of this argument.) A new team member can understand it. The whole composition is grep-able. If we change --signal once in the file, every appearance updates. If we want to A/B test the easing curve, we change one cubic-bezier and re-render.
This is the trade I keep coming back to: After Effects optimizes for the moment of authorship. HTML optimizes for the next ten years of the file's life. Both are valid. But increasingly, the second one is the one that matters, because the file is going to be edited by someone other than its original author, and that someone might be a model.
Variations from the same template
Once you have the pattern — --hf-time clock, paused animations, calc-driven delays, character splits, cinematic ease — every new composition is a variation. The 80 lines above turn into:
- A 6-second product reveal by swapping the title and adding a third beat.
- A 15-second testimonial by adding a portrait image and an inset quote.
- A nine-second loop for a social ad by changing the duration and the resolution.
- A vertical 9:16 by tweaking the
clamp()and the alignment.
None of these are new compositions, structurally. They are the same file, with different parameters. The substrate is text, the changes are diffs, and every variation is in the same git history as the original. This is the unlock that makes HTML the right substrate for motion: the file is the design system, the design system is the file, and the file is something you can keep changing for years.
Why the file is exactly this size
Eighty lines is not a magic number, but it is a deliberate one. A shorter composition starts to feel undercooked — title appears, title disappears, done. A longer composition starts to require structure: helper functions, sub-components, a build step. The eighty-line zone is where a single human can hold the entire piece in their head at once, and where an LLM can rewrite it without losing context.
I have found, across hundreds of compositions I have shipped this past year, that the sweet spot for a single-file editorial composition is somewhere between 60 and 140 lines. Below 60 the composition feels thin. Above 140 the file starts to drift; bugs creep in; the agent loop slows down because the model has to keep more in working memory.
When a composition wants to be larger than 140 lines, that is the signal to start extracting. A reusable lower third becomes its own file. A chart component lives in components/chart.html and is included via fetch. The brand tokens live in brand.css. The discipline is the same as software: extract when complexity demands it, not before.
A note on what is missing
If you read the listing carefully, you will notice things that are not there. There is no JavaScript animation library. No GSAP, no Anime, no Motion. There is no Three.js even though the title has a rotateX effect that looks 3D-ish (it is just CSS 3D transform). There is no canvas, no SVG (except indirectly through the way fonts render). There is no requestAnimationFrame loop. There is no shader.
This is intentional. The composition demonstrates that for an enormous category of editorial motion graphics — perhaps the majority of what motion designers actually ship in their day-to-day — the browser's built-in primitives are enough. CSS animations plus a single character-split script gets you most of the way to "professional." The frameworks come in when you have a specific need the primitives cannot meet, and in editorial work that need is rare.
I am not arguing against frameworks. GSAP is wonderful. Anime is delightful. Lottie is the right answer for some specific shapes of work. But the reflex of "I need motion, therefore I need a framework" is one of the things that makes motion design feel inaccessible to web developers who could otherwise produce excellent work. The primitives are sitting there. You can start.
Open your editor — or drop the listing straight into the HyperFrames playground and skip the install. Run the render. Then change one number. See what moves.
Cite this postBibTeX · APA · Markdown
@misc{okafor2026motion,
author = {Marcus Okafor},
title = {Motion graphics in 80 lines},
year = {2026},
url = {https://hyperframes.video/blog/motion-graphics-in-80-lines},
note = {HyperFrames blog}
}Marcus Okafor. (2026, May 7). Motion graphics in 80 lines. HyperFrames. https://hyperframes.video/blog/motion-graphics-in-80-lines
[Motion graphics in 80 lines](https://hyperframes.video/blog/motion-graphics-in-80-lines) — Marcus Okafor, 2026
Marcus leads design and motion at HyperFrames. Before that he shipped editorial motion for newsrooms and product launches. He thinks every easing curve has a personality.
How to animate your logo (without After Effects)
A logo reveal in pure CSS — spring overshoot, wordmark stagger, and a render straight to MP4. No timeline tool, no plugins.
Scroll-driven video: turning timelines into scroll positions
CSS scroll-driven animations finally went baseline in 2026. A practical tutorial on mapping video timelines to scroll positions, when to use scroll vs video, and the hybrid pattern we use on hyperframes.dev.
Stripe-style payment success animation in pure CSS
The animated checkmark, the elastic ring, the confetti — the full Stripe-style payment success animation in CSS. Free source, MP4 export.
Building with HyperFrames? Come hang out.
We're on GitHub, in Discord, and the playground is one click away. Bring weird ideas — we collect them.