HTML clips
HTML/CSS overlays with params, frame-exact.
project.dispatch({
type: "clip/add",
payload: {
kind: "html",
id: "lower-third",
trackId: "overlay",
startUs: 1_000_000,
durationUs: 4_000_000,
template: `
<div style="display:flex;align-items:center;gap:16px;height:100%;padding:0 28px;
border-radius:16px;background:linear-gradient(90deg,#e91e63,#9c27b0);
color:#fff;font:700 40px sans-serif">
{{name}} <span style="font:400 26px sans-serif;opacity:.8">{{role}}</span>
</div>`,
params: { name: "Ada Lovelace", role: "Analyst" },
widthPx: 640,
heightPx: 96,
transform: { x: 0.32, y: 0.85 },
},
});An HTML clip turns markup and CSS into an image at widthPx × heightPx, then places it like any other clip. Keyframes, effects and transitions apply to it. Use it for lower thirds, badges, cards and anything else CSS can lay out.
Fields
| Field | Default | Meaning |
|---|---|---|
template | required | Markup with {{name}} placeholders |
params | {} | Values for the placeholders: strings, numbers or booleans |
widthPx, heightPx | Composition size | Raster size in composition pixels. Content outside it is cut off |
animated | absent (false) | Render every frame so CSS animations play. See Animated templates |
animationOffsetUs | absent (0) | Animation time at the clip's start. clip/split sets it on the right half |
transform positions the raster's center, and scale: 1 fits the raster inside the composition, like any other clip. See Clips.
Params
{{name}} is replaced with params.name, HTML-escaped. Params are always text, never markup. A placeholder without a param stays in the output as written.
// params merge: only "role" changes
project.dispatch({ type: "clip/set-property", payload: { clipId: "lower-third", params: { role: "Programmer" } } });One template with different params is how to render many variants of the same video. See Templates.
Changing the markup
project.dispatch({
type: "clip/set-property",
payload: {
clipId: "lower-third",
template: `<b style="color:{{color}};font:700 48px sans-serif">{{label}}</b>`,
params: { label: "New", color: "#ffd400" },
unsetParams: ["name", "role"],
widthPx: 600,
heightPx: null, // back to the composition height
},
});Field in clip/set-property | Meaning |
|---|---|
template | New markup. The clip keeps its id, keyframes, effects and transitions |
params | Merged into the current params |
unsetParams | Param names to delete |
widthPx, heightPx | New raster size. null returns to the composition size |
animated | Turn per-frame rendering on or off |
These fields are rejected with not-html on other clip kinds.
Markup rules
The template renders inside an SVG foreignObject, which isolates it from the page.
| Rule | Why |
|---|---|
Write well-formed XHTML: close every tag (<br/>, <img .../>), quote attributes, use & | The template is parsed as XML. Markup that doesn't parse fails to render |
| No external URLs | Nothing loads from the network inside the raster |
Images: src="asset:<id>" | The image asset's file is inlined |
Fonts: name a font asset's family in the CSS | Font assets whose family appears in the markup are embedded. System fonts work as usual |
data: URIs work | For anything that isn't an asset |
| Scripts don't run, CSS transitions don't play | Use @keyframes in an animated template |
project.dispatch({ type: "asset/add", payload: { id: "logo", kind: "image", src: "/media/logo.png" } });
project.dispatch({
type: "clip/add",
payload: {
kind: "html",
id: "badge",
trackId: "overlay",
startUs: 0,
durationUs: 3_000_000,
template: `<img src="asset:logo" style="width:100%;height:100%;object-fit:contain"/>`,
widthPx: 200,
heightPx: 200,
transform: { x: 0.9, y: 0.12 },
},
});A reference to an asset id that doesn't exist fails the render.
Animated templates
With animated: true, the template renders at the clip's time on every frame. CSS @keyframes play in preview, follow the playhead when you scrub, and export frame by frame.
project.dispatch({
type: "clip/add",
payload: {
kind: "html",
id: "follow",
trackId: "overlay",
startUs: 0,
durationUs: 4_000_000,
animated: true,
widthPx: 900,
heightPx: 260,
template: `<div>
<style>
@keyframes rise { from { transform: translateY(120px); opacity: 0 } }
@keyframes fade { to { opacity: 0 } }
.card { --d: 0s; animation: rise .5s cubic-bezier(.2,.8,.2,1) both }
.btn { --d: 1s; animation: rise .4s ease-out both }
.out { --d: calc(var(--T) - .5s); animation: fade .5s both }
</style>
<div class="out">
<div class="card" style="font:700 64px sans-serif;color:#fff">New video</div>
<div class="btn" style="font:400 32px sans-serif;color:#fff">Follow for more</div>
</div>
</div>`,
},
});| CSS variable | Set by | Meaning |
|---|---|---|
--t | Renderer | Seconds into the animation |
--T | Renderer | The animation's total length in seconds |
--d | You | Delay for an element. It inherits, so set it on a parent to delay a group |
- The renderer pauses every animation and controls
animation-delay. Don't setanimation-delay; use--d. - Use
animation-fill-mode: both(orforwards) so elements keep their end state. - Time exits from the end with
--d: calc(var(--T) - .5s). Children inherit it, so give animated children their own--d, as.carddoes above. - After a split, the right half continues the animation instead of restarting it.
Static templates (the default) render once per change of template, params or size, so they cost nothing per frame. Animated templates render once per frame while visible. In preview, an animated clip may show the previous frame while the next one renders; exports and stills wait for every frame.
Export
| Export | HTML clips |
|---|---|
Preview, exportProject, renderProjectStill | Rendered on the main thread |
exportProjectInWorker, exportViaWorker | Static clips are rendered on the main thread before the export starts and sent to the worker. Animated clips are rendered on the main thread frame by frame, on request from the worker. No extra code needed |
| Server export | Supported |
Exports and stills wait for every HTML clip to render before drawing a frame. A template that fails to render fails the export.
If you run the worker protocol yourself, collectHtmlRasters(doc) pre-renders static clips and setHtmlRasterSource answers per-frame requests. See Worker export.
Notes
- Rasters render at the output density (export size ÷ composition size, or the preview's
outputSize), so text stays sharp on hi-DPI screens and upscaled exports. - Text antialiasing differs between platforms, so text pixels can differ slightly between machines.
substituteParams(template, params)andrasterizeHtml(options)are exported from@miraiclip/rendererif you need the same rendering elsewhere, for example for a template thumbnail.