Skip to main content

HTML5

The HTML5 asset renders a self-contained HTML, CSS, and JavaScript page inside a video clip. Use it for animated overlays, motion graphics, data visualisations, and anything you would otherwise build as a small single-page web app.

HTML5 replaces the deprecated html asset, which only rendered static markup. HTML5 runs in a real browser with a JavaScript runtime, a set of preloaded animation libraries, and deterministic frame capture. Use html5 for all new work.

Basic usage

A minimal HTML5 asset requires type set to html5 and an html string. Add css and js to style and animate it:

{
"asset": {
"type": "html5",
"html": "<div class=\"card\"><h1>{{title}}</h1></div>",
"css": ".card { font-family: 'Inter'; padding: 32px; color: #fff; }",
"js": "gsap.to('.card', { x: 200, duration: 1 });"
},
"start": 0,
"length": 4,
"width": 1920,
"height": 1080
}

Asset properties:

PropertyRequiredTypeDescription
typeYesstringMust be set to html5.
htmlYesstringThe page markup. Supports merge fields such as {{title}}. Max 1,000,000 characters.
cssNostringStylesheet, inlined into the page <head>. Max 500,000 characters.
jsNostringScript, run after the preloaded libraries are ready. Max 500,000 characters.

The clip-level width and height set the page's pixel dimensions and default to the output's dimensions. Every coordinate inside the page is measured in this pixel space.

Preloaded libraries

Four animation libraries are always available. Do not add any <script src> tags:

LibraryGlobalUse for
GSAPwindow.gsapThe primary animation library. Prefer timelines (gsap.timeline()) over loose tweens. Example.
anime.jswindow.animeAn alternative animation library. Example.
D3window.d3Data binding, scales, and SVG or DOM construction. Pair with GSAP for the animation. Example.
Lottiewindow.lottiePlaying Bodymovin JSON animations (SVG renderer). Example.

You cannot load other libraries over the network (see Sandbox restrictions). To use a different library, inline its source into your js and make sure it is seekable, as explained next.

Deterministic rendering

The renderer captures frames by seeking each animation to a timestamp, not by playing it in real time. The same input always produces the same output. The key requirement: your animation must be seekable.

These sources are driven automatically:

  • GSAP timelines and tweens
  • anime.js instances
  • Lottie animations loaded with lottie.loadAnimation(...)
  • CSS @keyframes, transitions, and Element.animate()

Anything whose state depends on real elapsed time is not seekable and captures a frozen or incorrect frame. Do not use:

  • setTimeout, setInterval, or requestAnimationFrame loops
  • Date.now(), performance.now(), or new Date()
  • gsap.call(), whose callbacks do not fire on seek

For content that changes over time, such as countdowns, tickers, or scene changes, render every state up front. Reveal each state with a staggered CSS animation (see the countdown example), or drive a value with a GSAP onUpdate tween.

The clip's length sets the duration. There is no auto-detection of animation length, so size your animation to run within the clip.

Custom animation engines

If you use a custom animation loop rather than one of the four libraries or CSS, expose a window.__shotstackSeek = (timeMs) => { /* render your scene at timeMs */ } function. The renderer calls it for each frame.

Sandbox restrictions

The page renders under a strict Content-Security-Policy (default-src 'none') in both Studio preview and cloud render. The renderer fetches nothing from the network at render time, so everything must be inlined:

  • No external scripts. Only the four preloaded libraries run. Inline any other library into your js.
  • No fetch or XMLHttpRequest. Bundle data as a literal in your js (see the bar chart example).
  • No remote images. Embed them as data: URIs. A data:image/svg+xml background works well.
  • No remote fonts. See below.

Fonts

The timeline.fonts array does not apply to HTML5 assets. It loads fonts only for the rich-text and rich-caption assets, and the CSP blocks remote @font-face URLs such as Google Fonts. To use a custom font, either:

  1. Inline it as a data: @font-face by base64-encoding the .woff2 or .ttf:
    @font-face {
    font-family: 'Brand';
    src: url('data:font/woff2;base64,<encoded font>') format('woff2');
    }
    .title { font-family: 'Brand', sans-serif; }
  2. Use a system family such as system-ui, Arial, or Georgia, which resolves in the render browser without loading a font file.

An unresolved font family silently falls back to the browser default. The render will not fail, but your text will not use the intended font. For a single styled line, the rich-text asset, which does use timeline.fonts, is simpler than embedding a font.

Sizing

The most important sizing rule: size the clip to the content, not to the canvas, then position it with offset.

The clip's width and height are the page's pixel dimensions, so match them to the content's actual size. A lower-third bar that is 560×120 belongs in a 560×120 clip positioned onto the canvas with offset, not in a 1920×1080 clip with the bar absolutely positioned in a corner.

{
"asset": {
"type": "html5",
"html": "<div class=\"bar\">…</div>",
"css": "html,body{margin:0;padding:0;width:560px;height:120px;background:transparent;overflow:hidden}.bar{width:560px;height:120px;…}"
},
"width": 560,
"height": 120,
"offset": { "x": -0.29, "y": -0.33 }
}
  • The clip width and height match the content's natural size.
  • The CSS html, body match the clip dimensions exactly.
  • The root element fills the body, with no absolute positioning inside the page.
  • offset places the clip on the canvas. See Positioning for the full model.

Size the clip to the full output only when the content genuinely fills the frame, such as full-screen charts, scene transitions, or titles.

A few rules apply either way:

  • Use fixed pixel values throughout (px, not vw, vh, or % on the root). Capture happens at the page's natural size, not a viewport.
  • Pin html, body to the clip dimensions with margin: 0; padding: 0; overflow: hidden.
  • The body is transparent by default, so the clip composites over the track below. Set an opaque background only when you need one.

Common HTML5 patterns

These are the patterns video editors reach for most often. Each one is a complete edit you can copy and adapt.

Animated lower-third (GSAP)

This name and role bar slides in, holds, and fades out. The clip is sized to the bar and positioned with offset, and the reusable text comes from a top-level merge.

{
"timeline": {
"background": "#0f172a",
"tracks": [
{
"clips": [
{
"asset": {
"type": "html5",
"html": "<div class=\"bar\"><div class=\"accent\"></div><div class=\"text\"><div class=\"name\" id=\"name\">{{name}}</div><div class=\"role\" id=\"role\">{{role}}</div></div></div>",
"css": "html,body{margin:0;padding:0;width:560px;height:120px;overflow:hidden;font-family:system-ui,sans-serif;background:transparent}.bar{display:flex;align-items:center;width:560px;height:120px;padding:0 32px;box-sizing:border-box;background:rgba(15,23,42,0.92);border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,0.45);opacity:0}.accent{width:6px;height:72px;background:linear-gradient(180deg,#22d3ee,#a78bfa);border-radius:3px;margin-right:24px;transform:scaleY(0);transform-origin:top}.text{display:flex;flex-direction:column;color:#fff}.name{font-size:44px;font-weight:700;letter-spacing:-0.5px;opacity:0;transform:translateX(-12px)}.role{font-size:22px;font-weight:500;color:#94a3b8;margin-top:4px;opacity:0;transform:translateX(-12px)}",
"js": "const tl=gsap.timeline();tl.to('.bar',{opacity:1,duration:0.5,ease:'power2.out'},0).to('.accent',{scaleY:1,duration:0.5,ease:'power3.out'},0.2).to('#name',{opacity:1,x:0,duration:0.5,ease:'power2.out'},0.35).to('#role',{opacity:1,x:0,duration:0.5,ease:'power2.out'},0.5).to({},{duration:3.5}).to('.bar',{opacity:0,duration:0.5,ease:'power2.in'});"
},
"start": 0,
"length": 5,
"width": 560,
"height": 120,
"offset": { "x": -0.29, "y": -0.39 }
}
]
}
]
},
"merge": [
{ "find": "name", "replace": "Sarah Chen" },
{ "find": "role", "replace": "Head of Product" }
],
"output": { "format": "mp4", "resolution": "1080" }
}

Notice:

  • The clip is the size of the bar, not the canvas, so placement requires only an offset change.
  • html, body, and .bar are all 560×120. The bar is the page content, so there is no absolute positioning.
  • One GSAP timeline drives every step, including a trailing empty tween (.to({}, { duration: 3.5 })) that holds the final state before the fade.
  • The top-level merge array populates {{name}} and {{role}}. It is a sibling of timeline and output, not a clip property, which keeps the asset reusable.

Animated bar chart (D3 + GSAP)

D3 builds the SVG chart and GSAP animates the bars from the baseline. The data is a literal array, without a network fetch.

{
"timeline": {
"tracks": [
{
"clips": [
{
"asset": {
"type": "html5",
"html": "<div class=\"stage\"><h1>Sessions by country</h1><div class=\"sub\">Last 30 days</div><svg id=\"chart\" width=\"1600\" height=\"640\" viewBox=\"0 0 1600 640\"><defs><linearGradient id=\"g\" x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#22d3ee\"/><stop offset=\"100%\" stop-color=\"#0891b2\"/></linearGradient></defs></svg></div>",
"css": "html,body{margin:0;padding:0;width:1920px;height:1080px;overflow:hidden;font-family:system-ui,sans-serif;background:radial-gradient(ellipse at 20% 0%,#0f172a 0%,#03020b 100%);color:#e2e8f0}.stage{position:relative;width:1920px;height:1080px;padding:96px 160px;box-sizing:border-box}h1{margin:0;font-size:64px;font-weight:800;letter-spacing:-2px;color:#fff;opacity:0}.sub{margin-top:12px;font-size:24px;color:#94a3b8;opacity:0}#chart{position:absolute;top:280px;left:160px;opacity:0}.bar{fill:url(#g)}.label{font-size:22px;font-weight:700;fill:#fff;font-variant-numeric:tabular-nums}.cat{font-size:18px;fill:#94a3b8;text-transform:uppercase;letter-spacing:1px}",
"js": "const data=[{c:'US',v:48230},{c:'IN',v:32140},{c:'GB',v:21670},{c:'DE',v:18450},{c:'BR',v:15890},{c:'JP',v:14210},{c:'AU',v:11630}];const W=1600,H=640,M={t:20,r:120,b:60,l:120};const iw=W-M.l-M.r,ih=H-M.t-M.b;const x=d3.scaleBand().domain(data.map(d=>d.c)).range([0,iw]).padding(0.28);const y=d3.scaleLinear().domain([0,d3.max(data,d=>d.v)*1.05]).range([ih,0]);const g=d3.select('#chart').append('g').attr('transform',`translate(${M.l},${M.t})`);const bars=g.selectAll('rect').data(data).enter().append('rect').attr('class','bar').attr('x',d=>x(d.c)).attr('y',ih).attr('width',x.bandwidth()).attr('height',0).attr('rx',8);const labels=g.selectAll('text.label').data(data).enter().append('text').attr('class','label').attr('x',d=>x(d.c)+x.bandwidth()/2).attr('y',ih).attr('text-anchor','middle').text(d=>d.v.toLocaleString()).style('opacity',0);const cats=g.selectAll('text.cat').data(data).enter().append('text').attr('class','cat').attr('x',d=>x(d.c)+x.bandwidth()/2).attr('y',ih+34).attr('text-anchor','middle').text(d=>d.c);const tl=gsap.timeline();tl.to('h1',{opacity:1,duration:0.6,ease:'power3.out'},0.1).to('.sub',{opacity:1,duration:0.5,ease:'power2.out'},0.5).to('#chart',{opacity:1,duration:0.4,ease:'power2.out'},0.7);bars.each(function(d,i){tl.to(this,{attr:{y:y(d.v),height:ih-y(d.v)},duration:0.7,ease:'power2.out'},1.0+i*0.08)});labels.each(function(d,i){tl.to(this,{attr:{y:y(d.v)-14},opacity:1,duration:0.4,ease:'power2.out'},1.4+i*0.08)});"
},
"start": 0,
"length": 6,
"width": 1920,
"height": 1080
}
]
}
]
},
"output": { "format": "mp4", "resolution": "1080" }
}

Notice:

  • D3 builds the DOM and GSAP animates it. D3's own transitions work, but GSAP is the more reliable seek target.
  • Each bar starts collapsed (height: 0 at the baseline) and grows via an attr tween.
  • Bars stagger by 80 ms (1.0 + i * 0.08).

Staggered grid (anime.js)

A grid of tiles scales and rotates in from the centre. anime.js drives the choreography with its grid-aware anime.stagger helper.

{
"timeline": {
"background": "#0f172a",
"tracks": [
{
"clips": [
{
"asset": {
"type": "html5",
"html": "<div class=\"grid\"><div class=\"cell\"></div><div class=\"cell\"></div><div class=\"cell\"></div><div class=\"cell\"></div><div class=\"cell\"></div><div class=\"cell\"></div><div class=\"cell\"></div><div class=\"cell\"></div><div class=\"cell\"></div><div class=\"cell\"></div><div class=\"cell\"></div><div class=\"cell\"></div><div class=\"cell\"></div><div class=\"cell\"></div><div class=\"cell\"></div></div>",
"css": "html,body{margin:0;padding:0;width:900px;height:560px;overflow:hidden;background:transparent}.grid{display:grid;grid-template-columns:repeat(5,1fr);grid-template-rows:repeat(3,1fr);gap:22px;width:900px;height:560px;padding:40px;box-sizing:border-box}.cell{background:linear-gradient(135deg,#22d3ee,#a78bfa);border-radius:18px;opacity:0}",
"js": "anime({targets:'.cell',scale:[0,1],opacity:[0,1],rotate:['-45deg','0deg'],delay:anime.stagger(90,{grid:[5,3],from:'center'}),duration:800,easing:'easeOutBack'});"
},
"start": 0,
"length": 4,
"width": 900,
"height": 560,
"offset": { "x": 0, "y": 0 }
}
]
}
]
},
"output": { "format": "mp4", "resolution": "1080" }
}

Notice:

  • anime.stagger(90, { grid: [5, 3], from: 'center' }) delays each tile by its distance from the centre of the 5×3 grid, which produces the ripple.
  • The harness seeks the anime.js instance, so easeOutBack and every other easing resolve correctly at each captured frame.
  • The clip is 900×560, sized to the grid and centred with offset: { x: 0, y: 0 }. The 15 .cell divs fill it with a CSS grid.

Animated icon (Lottie)

Dropping an animated icon or sticker over footage is the most common editing use for Lottie. Export the animation from After Effects or LottieFiles, inline its Bodymovin JSON, and load it with lottie.loadAnimation on a track above your video. Here a tick draws on over a background clip.

{
"timeline": {
"tracks": [
{
"clips": [
{
"asset": {
"type": "html5",
"html": "<div id=\"icon\"></div>",
"css": "html,body{margin:0;padding:0;width:360px;height:360px;overflow:hidden;background:transparent}#icon{width:360px;height:360px}",
"js": "const animationData = { /* your exported Bodymovin JSON */ }; lottie.loadAnimation({ container: document.getElementById('icon'), renderer: 'svg', loop: false, autoplay: false, animationData });"
},
"start": 0,
"length": 4,
"width": 360,
"height": 360,
"offset": { "x": 0, "y": 0 }
}
]
},
{
"clips": [
{
"asset": {
"type": "video",
"src": "https://shotstack-assets.s3.amazonaws.com/footage/skateboarder.mp4"
},
"start": 0,
"length": 4
}
]
}
]
},
"output": { "format": "mp4", "resolution": "1080" }
}

Notice:

  • The icon sits on tracks[0] (the top layer) above the video on tracks[1], and its body is transparent so the footage shows through.
  • Inline the Bodymovin JSON as animationData. There is no external .json URL, because the sandbox blocks network requests.
  • Use the SVG renderer (renderer: 'svg'). The canvas renderer is unavailable, and canvas output is not captured (see Common mistakes).
  • The harness seeks every animation registered with lottie.loadAnimation, so autoplay: false is correct. The clip length controls how much of the animation plays.

Countdown (pure CSS)

A countdown shows different content at different times, so seekability matters. Instead of a timer, render every number up front, hide them all, and give each its own CSS animation slot with a staggered animation-delay.

{
"asset": {
"type": "html5",
"html": "<div class=\"stage\"><div id=\"n10\" class=\"num\">10</div><div id=\"n9\" class=\"num\">9</div><div id=\"n8\" class=\"num\">8</div><div id=\"n7\" class=\"num\">7</div><div id=\"n6\" class=\"num\">6</div><div id=\"n5\" class=\"num\">5</div><div id=\"n4\" class=\"num\">4</div><div id=\"n3\" class=\"num\">3</div><div id=\"n2\" class=\"num\">2</div><div id=\"n1\" class=\"num\">1</div></div>",
"css": "html,body{margin:0;width:1920px;height:1080px;background:#000;font-family:system-ui,sans-serif;overflow:hidden}.stage{position:relative;width:1920px;height:1080px}.num{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:280px;font-weight:900;color:#fff;opacity:0}@keyframes pop{0%,100%{opacity:0;transform:scale(1.3)}10%,90%{opacity:1;transform:scale(1)}}#n10{animation:pop 1s linear 0s 1 both}#n9{animation:pop 1s linear 1s 1 both}#n8{animation:pop 1s linear 2s 1 both}#n7{animation:pop 1s linear 3s 1 both}#n6{animation:pop 1s linear 4s 1 both}#n5{animation:pop 1s linear 5s 1 both}#n4{animation:pop 1s linear 6s 1 both}#n3{animation:pop 1s linear 7s 1 both}#n2{animation:pop 1s linear 8s 1 both}#n1{animation:pop 1s linear 9s 1 both}"
},
"start": 0,
"length": 10,
"width": 1920,
"height": 1080
}

Notice:

  • One element per state (<div id="n10">10</div>, <div id="n9">9</div>, and so on). Never mutate textContent from JS.
  • Each animation is staggered by animation-delay (0s, 1s, 2s, …) into its own one-second slot.
  • animation-fill-mode: both combined with position: absolute; inset: 0 means only the number currently mid-animation is visible.

The same pattern scales to scene transitions, tickers, and animated lists.

Common mistakes

  • Time-based JavaScript that is not seekable. setTimeout, setInterval, requestAnimationFrame loops, Date.now(), and gsap.call() do not run during capture. Use GSAP, anime.js, Lottie, or CSS.
  • Using <canvas>. Frame capture serialises the page's DOM. Canvas pixels live in the backing store rather than the DOM, so they can be captured as empty. Use SVG or positioned DOM elements instead: animated <div> elements or SVG <circle> for particles, D3 and SVG for charts, and CSS or SVG filters for pixel effects. If the effect genuinely needs canvas, render it as a video or image asset instead.
  • Mismatched dimensions. If the clip is 1920×1080 but the CSS sets body { width: 1280px }, content is cropped or stretched. Pin html, body to the clip dimensions.
  • Expecting timeline.fonts to work. It does not apply to HTML5 assets. Inline the font or use a system family.
  • A white background over a video. The body is transparent by default; setting an opaque background hides the track below.

When to use HTML5

HTML5 is the most capable asset, but it is also the heaviest to render. For simpler needs, a lighter asset renders faster:

NeedUse
A single line of styled textrich-text
A few static shapessvg or shapes
Animated motion graphicshtml5 with GSAP
Data visualisations (charts, dashboards)html5 with D3 and GSAP
A Lottie animationhtml5 with lottie.loadAnimation(...)
Text effects beyond rich-texthtml5 with GSAP

Use HTML5 when the alternative would be an unwieldy stack of separate tracks, or when the design genuinely needs DOM-style layout such as flexbox, grid, or layered backgrounds with shadows.