Animated poll results as MP4
Build a poll results animation that renders to deterministic MP4: four horizontal bars race to their final percentages, then a winner ribbon sweeps in.
A poll results animation is a four-bar horizontal race that ends with the winning option getting a ribbon. The good version reads in under three seconds, holds its result for two, and exports as a clean MP4 you can drop into a tweet, a Slack channel, or a Reels carousel without a video editor.
Below is the polished pattern. The bars sweep out to their final widths on cubic ease-out, the percentages tick up in sync, and a small "WINNER" ribbon slides across the leading row once the bars have settled.
Why a horizontal bar, not a pie
Pie charts encode percentages as angle. The eye is bad at angles, especially at the 30–40% range where most "interesting" poll results live. A horizontal bar encodes the same data as length, which the eye reads with near-perfect accuracy. For four options, the bar is also faster to animate — there is one axis to interpolate, not two.
The other reason: bars stack vertically, which means labels are left-aligned and read like a list. A poll is a ranked list. Use the chart form that matches the data shape.
Driving the animation from hf-seek
Every frame of a HyperFrames render is triggered by a hf-seek event. e.detail.time is the current frame's time in seconds. There is no requestAnimationFrame loop, no setTimeout, no CSS time-based keyframes — just a function from t to DOM state.
addEventListener('hf-seek', (e) => {
const t = e.detail.time;
// bars race from 0 to final over 2.8s, cubic ease-out
const p = Math.min(1, t / 2.8);
const ease = 1 - Math.pow(1 - p, 3);
// ribbon slides in at t=3.2s over 0.6s
const rp = Math.max(0, Math.min(1, (t - 3.2) / 0.6));
});This is the entire timing contract. If you read the deterministic rendering doc, you'll see why: the renderer needs to know the visual state at any t without history. A setInterval-based count-up would produce different output on every render because it depends on wall-clock drift.
The bar fill
Width-based animation, not transform-based. Both work, but width lets you keep the rounded border-radius clipping the fill — a scaleX(0) element with transform-origin: left distorts the radius on the right edge. For a chart this small the difference is visible.
.track { height: 18px; background: #ece9e0; border-radius: 9px; overflow: hidden; }
.fill { height: 100%; width: 0%; border-radius: 9px; background: #2b66ff; }The track is the neutral cream-on-paper color, the fill is one of the brand accents. Each row gets its own color via a data attribute. Don't randomize — assign deliberately so the winning row's color matches your campaign.
The percentage counter
The number to the right of each bar ticks up alongside its fill. Both share the same ease value, so the visual length and the numeric value never disagree. This is harder than it sounds — if you animate them independently with two different durations, the eye catches the discrepancy as a "wrong" feeling even when it can't name it.
const f = +row.dataset.final;
row.querySelector('.fill').style.width = (f * ease) + '%';
row.querySelector('.pct').textContent = Math.round(f * ease) + '%';Tabular numerals are required. Without font-variant-numeric: tabular-nums, the digits change width as they tick — 1 is narrower than 8, the layout shifts, the eye reads "broken." See the easing reference for why cubic ease-out beats every other curve for this kind of reveal.
The winner ribbon
After all four bars settle (t = 3.0s), the ribbon sweeps in on the leading row. It enters from the left, behind the label, and slides across to sit on top of the bar. The sweep takes 600ms and uses the same cubic ease-out as the bars — the entire piece feels like one motion, not two.
The ribbon is a high-contrast inverted chip: ink background, cream text, all-caps tracked-out monospace. It exists to draw the eye back to the row that matters, after the bars have done their length comparison.
Data shape
For a programmatic pipeline (one render per poll), keep the input shape narrow. Three fields per option, plus the question. The renderer template reads JSON, interpolates into HTML, and the hf-seek script is identical across all polls.
<div class="row" data-final="{{percent}}" data-c="{{color}}">
<div class="lbl">{{label}}</div>
<div class="track"><div class="fill"></div></div>
<div class="pct">0%</div>
</div>This is the pattern for programmatic video from data — one HTML template, N rows of JSON, N MP4s. A daily standup poll, a Twitter poll archive, a customer survey roll-up — same template, different data.
Render to MP4
Save the polished version as poll.html, then:
hyperframes render poll.html --out clip.mp4 --duration 6 --fps 30Six seconds at 30 fps gives you 2.8s of bar race, a 400ms beat, the 600ms ribbon sweep, and a 2.2s hold on the final state. That hold is critical — short videos on social autoplay loop instantly, and a clean final frame is what gets screenshotted. See the quickstart for installation and the playground to iterate on the HTML interactively before committing it to a batch.
FAQ
How do I generate one MP4 per poll automatically?
Build an HTML template with placeholders, then loop over your poll dataset and write one HTML file per poll. Run hyperframes render against each. The programmatic video recipe walks through the full pipeline including filename templating and parallel rendering.
Can I show more than four options?
Yes — the layout is grid-based, so adding rows scales vertically. Above six options the card gets tall enough that the eye stops reading top-to-bottom and starts scanning. If you have eight options, either bucket the bottom four into "Other" or break into two columns.
Why not use Chart.js or D3?
You can. For a chart this constrained — one axis, fixed number of rows, brand colors — a library adds 80kb to render four divs. The HTML version is 60 lines, has no dependencies, and is trivial to drive from hf-seek. Libraries become worth it around the complexity of stacked area charts or live legends.
How do I match my brand colors?
The example uses HyperFrames brand tokens. Replace #2b66ff, #1f8a5b, #ff3b1f, #0a0a0a with your palette. The variable knobs above let you preview a single accent color; for a full brand swap, replace all four and the cream background #f6f5f1.
Can the bars race in different speeds?
Yes, but it usually looks worse. The visual contract is "all options finish at the same moment, but their final lengths differ." Staggered finishes feel like a competition, not a result. If you want emphasis, hold the bars for 400ms and then slide in the winner ribbon — that's a result reveal, not a race.
Related
- Animated bar chart tutorial — vertical variant, same easing
- Animated comparison tables — when the data is 2x2, not 4x1
- Easing curves cheatsheet — why cubic ease-out is the right call here
Cite this postBibTeX · APA · Markdown
@misc{tanaka2026animated,
author = {Kira Tanaka},
title = {Animated poll results as MP4},
year = {2026},
url = {https://hyperframes.video/blog/animated-poll-results-bars},
note = {HyperFrames blog}
}Kira Tanaka. (2026, May 21). Animated poll results as MP4. HyperFrames. https://hyperframes.video/blog/animated-poll-results-bars
[Animated poll results as MP4](https://hyperframes.video/blog/animated-poll-results-bars) — Kira Tanaka, 2026
Kira works on the render core: headless Chromium scheduling, frame capture, and the encoder pipeline. She cares about reproducible builds and small numbers next to the word "variance."
Animated flight itinerary cards in HTML
Build an animated boarding pass video: a plane traces an arc from origin to destination, gate and time settle in, the pass folds out — rendered as MP4.
Turn any SVG animation into a real MP4
SMIL, CSS, JS-driven — every flavor of SVG animation, rendered to a deterministic MP4 you can ship anywhere.
Animate a stock chart with real data
A scrubbing-line stock chart from a JSON of price points. The windowing trick, the easing, and the render.
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.