Boardroom clean
Use it for Pipeline or QBR story
Read the prompt
Build a 15-second animated video in the "Boardroom clean" style: a white slide where a bar chart builds in, one bar is spotlighted, and three KPI tiles count up beside it.
It's for a pipeline or QBR story: one chart that builds, then the three numbers that matter.
MY CONTENT (fill these in; delete a line to keep the default)
- What the video says, in one sentence: [e.g. "Inbound drove 45% of new pipeline this quarter"]
- Numbers and labels: [a hero number plus 2 or 3 supporting numbers, each with a short label]
- Source line: [where the numbers come from]
- Brand colors: [hex codes for background, text and 2 or 3 accents]. Default: #6688ad, #545454, #292424, #7ba69d, #dc685a
- Font: [font name]. Default: Poppins
- Format: [16:9, 1:1 or 9:16]. Default: 16:9
HOW TO BUILD IT
1. Start from the reference code at the end of this message. It already works. Keep its structure: the CONFIG block, the fixed-size stage, build(), render(t) and the player at the bottom. Put my content, colors, font and format into CONFIG, then adjust the layout so everything fits.
2. Every frame is a pure function of time: render(t) draws the frame for t seconds. Don't add CSS animations, transitions, setTimeout or setInterval. That's what lets the video be scrubbed, recorded cleanly and captured frame by frame.
3. If you're in Claude Motion, make it a Motion animation and use the reference for the look and the timing. Anywhere else, return one self-contained HTML artifact with no external scripts. The Google Fonts link is the only outside request.
4. Keep record mode working. The Record mode button (or the R key) hides every control and holds the first frame. Clicking the stage or pressing Space plays it once and holds the last frame. Esc exits.
5. No sound. About 15 seconds, with the final frame held for at least 2 seconds.
6. Text has to read on a phone. On the 1920x1080 stage nothing goes under 30px, labels are 40px or larger, and headline numbers 120px or larger. No text overlaps other text, runs off the stage, or leaves one word alone on the last line of a heading. If my content doesn't fit, cut words or add a scene. Don't shrink the type.
7. Keep it to one chart and up to three KPI tiles. Spotlight the one bar that carries the message and dim the rest. The bar data lives in CONFIG.bars.
Optional: before writing any code, give me a five-line storyboard (time, what's on screen, what moves) and wait for my OK.
When it's done, tell me in two lines how to get an MP4: open Record mode, start a screen recording of the stage (Cmd+Shift+5 on a Mac, or Loom), then press Space.
REFERENCE CODE: Boardroom clean
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Boardroom clean</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap">
<style>
/* Player: fixed-size stage, scaled to fit the window. Keep as is. */
html,body{margin:0;height:100%;background:#111;overflow:hidden}
#stage{position:absolute;left:50%;top:50%;overflow:hidden;box-sizing:border-box;background:var(--bg);color:var(--ink);font-family:var(--font)}
#stage *{box-sizing:border-box}
#stage::after{content:"";position:absolute;inset:0;background:var(--bg);opacity:var(--fade,0);pointer-events:none;z-index:99}
#ui{position:fixed;left:50%;bottom:12px;transform:translateX(-50%);display:flex;gap:8px;align-items:center;padding:8px 12px;border-radius:10px;background:rgba(0,0,0,.7);font:13px system-ui,sans-serif;color:#eee;z-index:9}
#ui button{font:inherit;padding:5px 10px;border:0;border-radius:6px;cursor:pointer}
body.rec #ui{display:none} body.rec{cursor:none}
/* Style */
#stage{padding:90px 110px 70px;display:flex;flex-direction:column}
.eyebrow{font-size:36px;font-weight:600;letter-spacing:.08em;color:var(--c1)}
h1{margin:14px 0 0;font-size:72px;line-height:1.1;font-weight:600;text-wrap:balance}
.body{flex:1;display:grid;grid-template-columns:1.25fr 1fr;grid-template-rows:minmax(0,1fr);gap:90px;margin-top:56px;min-height:0}
.chart{display:grid;grid-template-rows:minmax(0,1fr) auto auto;min-height:0}
.plot{display:flex;align-items:flex-end;gap:36px;border-bottom:4px solid var(--ink);position:relative}
.col{flex:1;height:100%;display:flex;flex-direction:column;justify-content:flex-end;align-items:center}
.val{font-size:52px;font-weight:700;margin-bottom:12px}
.bar{width:100%;background:var(--grey);border-radius:10px 10px 0 0}
.col.hi .bar{background:var(--c1)} .col.hi .val{color:var(--c1)}
.labels{display:flex;gap:36px;margin-top:16px}
.labels div{flex:1;text-align:center;font-size:38px;font-weight:600;color:var(--muted)}
.axis{font-size:36px;color:var(--muted);text-align:center;margin-top:8px}
.callout{position:absolute;right:0;top:0;background:var(--ink);color:var(--bg);font-size:40px;font-weight:600;padding:16px 28px;border-radius:12px;white-space:nowrap}
.kpis{display:flex;flex-direction:column;justify-content:center;gap:40px}
.kpis h2{margin:0;font-size:44px;font-weight:600;color:var(--muted)}
.kpi{display:flex;align-items:center;gap:32px;border-left:10px solid var(--c2);padding-left:32px}
.kpi b{flex:none;width:270px;font-size:96px;line-height:1;font-weight:700}
.kpi span{font-size:40px;line-height:1.25}
.source{font-size:32px;color:var(--muted);margin-top:36px}
.square .kpis{gap:22px} .square .kpi b{font-size:76px;width:220px}
.tall .body,.square .body{grid-template-columns:1fr;grid-template-rows:minmax(0,1fr) auto;gap:70px}
</style>
</head>
<body>
<div id="stage">
<div class="eyebrow" id="eyebrow"></div>
<h1 id="title"></h1>
<div class="body">
<div class="chart">
<div class="plot" id="plot"><div class="callout" id="callout"></div></div>
<div class="labels" id="labels"></div>
<div class="axis" id="axis"></div>
</div>
<div class="kpis" id="kpis"><h2 id="kpiHead"></h2></div>
</div>
<div class="source" id="source"></div>
</div>
<div id="ui"><button id="b-play">Play / Pause</button><button id="b-replay">Replay</button><input id="scrub" type="range" min="0" max="1000" value="0"><button id="b-rec" title="Hides the controls. Click the stage or press Space to play once. Esc exits.">Record mode</button></div>
<script>
/* ---------- Your content: edit here ---------- */
const CONFIG = {
format: "16:9", // "16:9" | "1:1" | "9:16"
duration: 15, // seconds
font: "Poppins",
colors: { bg: "#ffffff", ink: "#292424", muted: "#545454", grey: "#d9dee6", c1: "#6688ad", c2: "#7ba69d", c3: "#dc685a" },
eyebrow: "Q3 PIPELINE REVIEW",
title: "Where new pipeline came from this quarter",
axis: "New opportunities by source",
bars: [ { label: "Inbound", value: 142, highlight: true }, { label: "Outbound", value: 96 },
{ label: "Partner", value: 48 }, { label: "Events", value: 31 } ],
callout: "45% of new opportunities",
kpiHead: "This quarter:",
kpis: [ { value: 24, suffix: "%", text: "win rate on new opportunities" },
{ value: 38, prefix: "$", suffix: "K", text: "average deal size" },
{ value: 46, text: "days in the average sales cycle" } ],
source: "Sample data",
};
/* ---------- Helpers ---------- */
const $ = (id) => document.getElementById(id);
const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
const prog = (t, start, dur) => clamp((t - start) / dur); // 0 to 1 between start and start+dur
const ease = (x) => 1 - Math.pow(1 - x, 3); // ease-out cubic
const show = (el, x, dy = 30) => { el.style.opacity = x; el.style.transform = `translateY(${(1 - x) * dy}px)`; };
/* ---------- Build the DOM once from CONFIG ---------- */
function build() {
$("eyebrow").textContent = CONFIG.eyebrow; $("title").textContent = CONFIG.title;
$("axis").textContent = CONFIG.axis; $("callout").textContent = CONFIG.callout;
$("kpiHead").textContent = CONFIG.kpiHead; $("source").textContent = CONFIG.source;
for (const b of CONFIG.bars) {
$("plot").insertAdjacentHTML("beforeend", `<div class="col${b.highlight ? " hi" : ""}"><div class="val"></div><div class="bar"></div></div>`);
$("labels").insertAdjacentHTML("beforeend", `<div>${b.label}</div>`);
}
for (const k of CONFIG.kpis) $("kpis").insertAdjacentHTML("beforeend", `<div class="kpi"><b></b><span>${k.text}</span></div>`);
}
/* ---------- Draw one frame for time t (seconds). Pure: same t, same frame. ---------- */
function render(t) {
show($("eyebrow"), ease(prog(t, 0.2, 0.8))); show($("title"), ease(prog(t, 0.4, 0.9)));
const max = Math.max(...CONFIG.bars.map((b) => b.value));
const cols = [...document.querySelectorAll(".col")];
cols.forEach((c, i) => {
const p = ease(prog(t, 1.6 + i * 0.35, 1.3));
c.querySelector(".bar").style.height = (p * CONFIG.bars[i].value / max * 74) + "%";
c.querySelector(".val").textContent = Math.round(p * CONFIG.bars[i].value);
c.querySelector(".val").style.opacity = p > 0 ? 1 : 0;
const dim = CONFIG.bars[i].highlight ? 0 : prog(t, 4.6, 0.6); // spotlight the highlighted bar
c.style.opacity = 1 - dim * 0.55;
});
show($("labels"), prog(t, 1.4, 0.6), 10); show($("axis"), prog(t, 1.6, 0.6), 10);
const cp = ease(prog(t, 5.0, 0.7)); $("callout").style.opacity = cp;
$("callout").style.transform = `scale(${0.85 + 0.15 * cp})`;
show($("kpiHead"), ease(prog(t, 6.4, 0.7)));
[...document.querySelectorAll(".kpi")].forEach((k, i) => {
const p = ease(prog(t, 7.0 + i * 0.9, 0.8));
k.style.opacity = p; k.style.transform = `translateX(${(1 - p) * 60}px)`;
k.querySelector("b").textContent = (CONFIG.kpis[i].prefix || "") + Math.round(CONFIG.kpis[i].value * ease(prog(t, 7.0 + i * 0.9, 1.2))) + (CONFIG.kpis[i].suffix || "");
});
show($("source"), prog(t, 2.0, 0.8), 0);
}
/* ---------- Player: keep as is ---------- */
const SIZES = { "16:9": [1920, 1080], "1:1": [1440, 1440], "9:16": [1080, 1920] };
const [W, H] = SIZES[CONFIG.format] || SIZES["16:9"], D = CONFIG.duration;
const stage = $("stage"), scrub = $("scrub");
stage.style.width = W + "px"; stage.style.height = H + "px";
stage.classList.toggle("tall", H > W); stage.classList.toggle("square", H === W);
for (const [k, v] of Object.entries(CONFIG.colors)) stage.style.setProperty("--" + k, v);
stage.style.setProperty("--font", `"${CONFIG.font}", system-ui, sans-serif`);
let t = 0, playing = true, rec = false, start = performance.now();
const fit = () => { stage.style.transform = `translate(-50%,-50%) scale(${Math.min(innerWidth / W, innerHeight / H)})`; };
function draw(time) { t = time; render(t); stage.style.setProperty("--fade", rec ? 0 : clamp((t - D + 0.5) / 0.5)); scrub.value = t / D * 1000; }
window.seek = (time) => { playing = false; draw(time); }; // frame-by-frame capture: seek(0), seek(1/30), ...
function play(from = 0) { start = performance.now() - from * 1000; playing = true; }
function tick(now) {
if (playing) { let x = (now - start) / 1000; if (x >= D) { if (rec) { x = D; playing = false; } else { x = 0; play(0); } } draw(x); }
requestAnimationFrame(tick);
}
function setRec(on) { rec = on; document.body.classList.toggle("rec", on); seek(0); }
$("b-play").onclick = () => (playing ? (playing = false) : play(t >= D ? 0 : t));
$("b-replay").onclick = () => play(0);
$("b-rec").onclick = () => setRec(true);
scrub.oninput = () => seek(scrub.value / 1000 * D);
stage.onclick = () => { if (rec) play(0); };
addEventListener("keydown", (e) => {
if (e.code === "Space") { e.preventDefault(); if (rec) play(0); else if (playing) playing = false; else play(t >= D ? 0 : t); }
if (e.key === "r" || e.key === "R") setRec(!rec);
if (e.key === "Escape") { setRec(false); play(0); }
});
addEventListener("resize", fit); fit(); build(); draw(0); requestAnimationFrame(tick);
</script>
</body>
</html>
```
