
# 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:

```json
{
    "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](/docs/guide/architecting-an-application/merging-data) 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](https://gsap.com/)              | `window.gsap`  | The primary animation library. Prefer timelines (`gsap.timeline()`) over loose tweens. [Example](#animated-lower-third-gsap). |
| [anime.js](https://animejs.com/)       | `window.anime` | An alternative animation library. [Example](#staggered-grid-animejs). |
| [D3](https://d3js.org/)                | `window.d3`    | Data binding, scales, and SVG or DOM construction. Pair with GSAP for the animation. [Example](#animated-bar-chart-d3--gsap). |
| [Lottie](https://airbnb.io/lottie/)    | `window.lottie`| Playing Bodymovin JSON animations (SVG renderer). [Example](#animated-icon-lottie). |

You cannot load other libraries over the network (see [Sandbox restrictions](#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](#countdown-pure-css)), 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.

:::note 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](#animated-bar-chart-d3--gsap)).
- **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`:
   ```css
   @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](/docs/guide/architecting-an-application/rich-text), 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.

```json
{
    "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](/docs/guide/architecting-an-application/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`.

<video autoPlay loop muted playsInline width="100%">
    <source src="https://d1uej6xx5jo4cd.cloudfront.net/documentation/html5/lower-third-gsap.mp4" type="video/mp4" />
</video>

```json
{
    "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.

<video autoPlay loop muted playsInline width="100%">
    <source src="https://d1uej6xx5jo4cd.cloudfront.net/documentation/html5/bar-chart-d3.mp4" type="video/mp4" />
</video>

```json
{
    "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.

<video autoPlay loop muted playsInline width="100%">
    <source src="https://d1uej6xx5jo4cd.cloudfront.net/documentation/html5/staggered-grid-anime.mp4" type="video/mp4" />
</video>

```json
{
    "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](https://lottiefiles.com/), 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.

<video autoPlay loop muted playsInline width="100%">
    <source src="https://d1uej6xx5jo4cd.cloudfront.net/documentation/html5/animated-icon-lottie.mp4" type="video/mp4" />
</video>

```json
{
    "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](#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`.

<video autoPlay loop muted playsInline width="100%">
    <source src="https://d1uej6xx5jo4cd.cloudfront.net/documentation/html5/countdown.mp4" type="video/mp4" />
</video>

```json
{
    "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:

| Need                                       | Use                                                                       |
| :----------------------------------------- | :------------------------------------------------------------------------ |
| A single line of styled text               | [rich-text](/docs/guide/architecting-an-application/rich-text)            |
| A few static shapes                        | [svg](/docs/guide/architecting-an-application/svg) or [shapes](/docs/guide/architecting-an-application/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](/docs/guide/architecting-an-application/positioning): Position and scale clips on the canvas
- [Animations](/docs/guide/architecting-an-application/animations): Animate clip properties over time
- [Merging Data](/docs/guide/architecting-an-application/merging-data): Populate merge fields like `{{title}}`
- [Rich Text](/docs/guide/architecting-an-application/rich-text): Lighter asset for styled text
