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:
| Property | Required | Type | Description |
|---|---|---|---|
type | Yes | string | Must be set to html5. |
html | Yes | string | The page markup. Supports merge fields such as {{title}}. Max 1,000,000 characters. |
css | No | string | Stylesheet, inlined into the page <head>. Max 500,000 characters. |
js | No | string | Script, 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:
| Library | Global | Use for |
|---|---|---|
| GSAP | window.gsap | The primary animation library. Prefer timelines (gsap.timeline()) over loose tweens. Example. |
| anime.js | window.anime | An alternative animation library. Example. |
| D3 | window.d3 | Data binding, scales, and SVG or DOM construction. Pair with GSAP for the animation. Example. |
| Lottie | window.lottie | Playing 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, andElement.animate()
Anything whose state depends on real elapsed time is not seekable and captures a frozen or incorrect frame. Do not use:
setTimeout,setInterval, orrequestAnimationFrameloopsDate.now(),performance.now(), ornew 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.
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
fetchorXMLHttpRequest. Bundle data as a literal in yourjs(see the bar chart example). - No remote images. Embed them as
data:URIs. Adata:image/svg+xmlbackground 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:
- Inline it as a
data:@font-faceby base64-encoding the.woff2or.ttf:@font-face {
font-family: 'Brand';
src: url('data:font/woff2;base64,<encoded font>') format('woff2');
}
.title { font-family: 'Brand', sans-serif; } - Use a system family such as
system-ui,Arial, orGeorgia, 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
widthandheightmatch the content's natural size. - The CSS
html, bodymatch the clip dimensions exactly. - The root element fills the body, with no absolute positioning inside the page.
offsetplaces 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, notvw,vh, or%on the root). Capture happens at the page's natural size, not a viewport. - Pin
html, bodyto the clip dimensions withmargin: 0; padding: 0; overflow: hidden. - The body is transparent by default, so the clip composites over the track below. Set an opaque
backgroundonly 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
offsetchange. html,body, and.barare 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
mergearray populates{{name}}and{{role}}. It is a sibling oftimelineandoutput, 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: 0at the baseline) and grows via anattrtween. - 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
easeOutBackand 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.celldivs 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 ontracks[1], and itsbodyis transparent so the footage shows through. - Inline the Bodymovin JSON as
animationData. There is no external.jsonURL, 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, soautoplay: falseis correct. The cliplengthcontrols 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 mutatetextContentfrom JS. - Each animation is staggered by
animation-delay(0s,1s,2s, …) into its own one-second slot. animation-fill-mode: bothcombined withposition: absolute; inset: 0means 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,requestAnimationFrameloops,Date.now(), andgsap.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. Pinhtml, bodyto the clip dimensions. - Expecting
timeline.fontsto 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:
| Need | Use |
|---|---|
| A single line of styled text | rich-text |
| A few static shapes | svg or shapes |
| Animated motion graphics | html5 with GSAP |
| Data visualisations (charts, dashboards) | html5 with D3 and GSAP |
| A Lottie animation | html5 with lottie.loadAnimation(...) |
| Text effects beyond rich-text | html5 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.
Related topics
- Positioning: Position and scale clips on the canvas
- Animations: Animate clip properties over time
- Merging Data: Populate merge fields like
{{title}} - Rich Text: Lighter asset for styled text