Miraiclip SDK
Creative

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

FieldDefaultMeaning
templaterequiredMarkup with {{name}} placeholders
params{}Values for the placeholders: strings, numbers or booleans
widthPx, heightPxComposition sizeRaster size in composition pixels. Content outside it is cut off
animatedabsent (false)Render every frame so CSS animations play. See Animated templates
animationOffsetUsabsent (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-propertyMeaning
templateNew markup. The clip keeps its id, keyframes, effects and transitions
paramsMerged into the current params
unsetParamsParam names to delete
widthPx, heightPxNew raster size. null returns to the composition size
animatedTurn 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.

RuleWhy
Write well-formed XHTML: close every tag (<br/>, <img .../>), quote attributes, use &amp;The template is parsed as XML. Markup that doesn't parse fails to render
No external URLsNothing 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 CSSFont assets whose family appears in the markup are embedded. System fonts work as usual
data: URIs workFor anything that isn't an asset
Scripts don't run, CSS transitions don't playUse @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 variableSet byMeaning
--tRendererSeconds into the animation
--TRendererThe animation's total length in seconds
--dYouDelay 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 set animation-delay; use --d.
  • Use animation-fill-mode: both (or forwards) 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 .card does 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

ExportHTML clips
Preview, exportProject, renderProjectStillRendered on the main thread
exportProjectInWorker, exportViaWorkerStatic 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 exportSupported

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) and rasterizeHtml(options) are exported from @miraiclip/renderer if you need the same rendering elsewhere, for example for a template thumbnail.

On this page