Branding with design tokens
Centralize brand colors, fonts, and motion ramps so every template renders on-brand.
A single brand.css imported by every template. Change one variable, every video re-renders on brand. No find-and-replace, no template-by-template audit.
What you'll learn
- Structuring brand tokens as CSS custom properties
- Sharing motion ramps (easing curves) as tokens, not magic numbers
- Wiring
brand.cssinto a HyperFrames template
Try the brand swap
Anatomy of brand.css
Three token families cover most needs: color, type, motion.
/* brand.css */
:root {
/* Color */
--brand-ink: #0f172a;
--brand-paper: #f8fafc;
--brand-accent: #7c3aed;
--brand-warn: #f59e0b;
--brand-success: #10b981;
/* Type */
--brand-display: "Inter Display", ui-sans-serif, system-ui;
--brand-body: "Inter", ui-sans-serif, system-ui;
--brand-mono: "JetBrains Mono", ui-monospace, monospace;
--brand-tight: -0.03em;
--brand-loose: 0.18em;
/* Motion */
--ease-out-soft: cubic-bezier(.22, 1, .36, 1);
--ease-in-out-snap: cubic-bezier(.65, 0, .35, 1);
--ease-spring: cubic-bezier(.2, .9, .25, 1.2);
--dur-quick: 280ms;
--dur-base: 480ms;
--dur-slow: 880ms;
}
body { font-family: var(--brand-body); color: var(--brand-ink); background: var(--brand-paper); }
h1, h2, h3 { font-family: var(--brand-display); letter-spacing: var(--brand-tight); }That's the entire brand surface. Templates consume tokens, never raw values.
Using brand.css in a template
Inline it for determinism. Headless Chrome won't fetch from the network during a render anyway, and inlining makes the template self-contained.
<!doctype html>
<html>
<head>
<style>{{!INCLUDE: brand.css}}</style>
<style>
.hero { display:grid; place-items:center; height:100vh; }
.hero h2 {
font-size: 88px;
animation: rise var(--dur-slow) var(--ease-out-soft) both;
}
@keyframes rise {
from { opacity:0; transform: translateY(16px); }
to { opacity:1; transform:none; }
}
</style>
</head>
<body>
<div class="hero"><h2>On brand, every frame.</h2></div>
</body>
</html>If your build step doesn't support {{!INCLUDE}}-style directives, a five-line script can concatenate brand.css into the template before passing it to hyperframes render:
import { readFileSync, writeFileSync } from "node:fs";
const brand = readFileSync("brand.css", "utf8");
const tpl = readFileSync("template.html", "utf8")
.replace("/*BRAND*/", brand);
writeFileSync("out.html", tpl);node inline-brand.mjs && hyperframes render out.html --out promo.mp4 --crf 18Per-render brand overrides
For multi-tenant scenarios (whitelabel customers, per-campaign accent colors), pass tokens as variants:
[
{ "out": "acme.mp4", "vars": { "ACCENT": "#ef4444", "INK": "#1c1917" } },
{ "out": "zonic.mp4", "vars": { "ACCENT": "#10b981", "INK": "#0f172a" } }
]Template:
<style>
:root {
--brand-accent: {{$ACCENT}};
--brand-ink: {{$INK}};
}
</style>One template, thousands of brand-compliant outputs, zero forks.
Lint your tokens
Bake brand-compliance checks into your existing CSS linter. A short Stylelint rule blocks raw hex codes inside template files:
{
"rules": {
"color-no-hex": [true, {
"message": "Use a --brand-* token instead of a raw hex.",
"ignoreFiles": ["brand.css"]
}]
}
}Now a designer can't accidentally ship #683AB7 when the brand color is #7c3aed.
Tweak it
- Add a
--brand-radius-*scale so card corners stay consistent. - Add a
--brand-shadow-*set so glow effects use the same falloff everywhere. - Version the brand file (
brand.v2.css) when you do a refresh — old renders still re-render identically againstbrand.v1.css.