---
title: "Text & HTML Elements"
canonical_url: https://docs.zvid.io/docs/structure/text-elements/
source: docs/structure/text-elements.md
content_revision: 6338386a1dd87e4a
---

# Text & HTML Elements

`TEXT` elements render text or HTML into a visual layer and compose it into the
video. They have two modes:

- **Plain text** — set `text` and style it with the `style` object.
- **HTML** — set `html` with your own markup. With the optional
  [`customCode`](#native-css--javascript-customcode) field you can attach
  **native CSS and JavaScript**, turning a `TEXT` element into a fully
  styled, animated mini web page.

> **HTML vs SVG elements**
HTML elements support text, badges, layouts, gradients, CSS/WAAPI animation,
and a [restricted inline SVG subset](#inline-svg-in-html) for vector shapes.
Existing [SVG elements](https://docs.zvid.io/docs/structure/svg-elements/) remain supported; their standalone
markup rules differ from the stricter SVG subtree allowed in `html`.

## Interface

```typescript
interface TextItem {
  type: "TEXT";
  text?: string;
  html?: string;
  style?: Record<string, string | number>;
  fitToBox?: boolean;
  customCode?: CustomCode;
  x?: number;
  y?: number;
  width?: number;
  height?: number;
  anchor?: Anchor;
  position?: PositionPreset;
  enterBegin?: number;
  enterEnd?: number;
  exitBegin?: number;
  exitEnd?: number;
  track?: number;
  opacity?: number;
  angle?: number;
  flipV?: boolean;
  flipH?: boolean;
  enterAnimation?: XFadeEffect | null;
  exitAnimation?: XFadeEffect | null;
}

interface CustomCode {
  css?: string;
  js?: string;
  animationDuration?: number;
}
```

## Required Fields

| Property         | Type     | Notes                               |
| ---------------- | -------- | ----------------------------------- |
| `type`           | `"TEXT"` | Case-insensitive in API validation. |
| `text` or `html` | `string` | At least one must contain content.  |

Placement, timing, layering, and animation fields are shared by all visual
elements — see [Common Element Properties](https://docs.zvid.io/docs/structure/common-properties/).

## Content

- `text` is plain text. The API rejects `<` and `>` in this field.
- `html` is your own HTML markup and takes priority over `text` when both are
  present. The hosted API accepts a **safe tag subset**: `div`, `span`, `p`,
  `br`, `b`, `strong`, `i`, `em`, `u`, `s`, `ul`, `ol`, `li`, `img`, and
  `canvas`, plus the SVG subset below. HTML elements accept `style`, `class`, `src`, `alt`, `width`, and `height`
  attributes. `img` sources must be public http(s) URLs or inline raster
  `data:image/…` URIs.
- `style` accepts CSS property names and string or number values; it styles the
  element's root container.
- `customCode` attaches native CSS and JavaScript — see below.

## Inline SVG in HTML

`html` accepts geometry-only SVG: `svg`, `g`, `defs`, gradients/stops, paths,
circles, ellipses, rectangles, lines, polylines, and polygons. Use supported
geometry/paint attributes and local gradient references such as `url(#brand)`.
Scripts, event handlers, `foreignObject`, `use`, `image`, external resources,
and SVG animation tags are rejected. Use HTML text around the SVG for labels,
and `customCode` CSS/JavaScript for animation.

## Text sizing and overflow

Set `width` and `height` in pixels when text must fit a defined box. By default,
the renderer preserves the authored typography, so long copy can be clipped.
Set `fitToBox: true` to shrink typography just enough to keep painted text
inside the declared box. Text that already fits is unchanged; this option
does not enlarge text or replace intentional layout and line breaks.
Fitting reduces type metrics to no less than half their authored size and
does not move fixed layout geometry. Very long copy or fixed negative offsets
can still overflow; inspect representative variable values and the final render.

This **element fragment** belongs in `payload.visuals`:

```json
{
  "type": "TEXT",
  "text": "A product title that may become longer",
  "width": 800,
  "height": 160,
  "position": "center-center",
  "fitToBox": true,
  "style": { "fontFamily": "Poppins", "fontSize": 64, "color": "#111111" }
}
```

See [Layout](https://docs.zvid.io/docs/concepts/layout/) for box sizing, anchors, and margins.

## Native CSS & JavaScript (`customCode`)

`customCode` lets you style, animate, and script the element with the same tools
you use on the web. The element's `html` is rendered in a headless browser, your
`css` and `js` run against it, and the result is captured as the element's visual
layer.

| Field               | Type     | Notes                                                                                                                                                          |
| ------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `css`               | `string` | Raw CSS injected into the page. Define `@keyframes` here and target the classes you used in the element's `html`.                                              |
| `js`                | `string` | JavaScript run **after** the content loads. Use it to manipulate the DOM or drive animations with the Web Animations API.                                      |
| `animationDuration` | `number` | Length of **one** animation loop in seconds (max `15`). Only that loop is captured and then looped to fill the element's timeline. Auto-detected when omitted. |

### How animation capture works

When `customCode` is present, Zvid captures a **single loop** of your animation
and lets FFmpeg repeat it for the element's on-screen duration. This means the
element's timeline length (`enterBegin` → `exitEnd`) does **not** increase
capture time. Billing still uses the final video's duration and resolution,
so a longer output can cost more credits. Set `animationDuration` to the exact length of one loop for
a seamless repeat; omit it to let Zvid auto-detect.

### Example: CSS keyframe animation

A pulsing badge — no `text`, just HTML targeted by a CSS class. The following
animation examples are element fragments for `payload.visuals`. Each recorded
preview includes its own complete project payload:

```json
{
  "type": "TEXT",
  "html": "<div class=\"badge\">LIMITED OFFER</div>",
  "position": "center-center",
  "enterBegin": 0,
  "exitEnd": 5,
  "customCode": {
    "css": ".badge { color: #ffffff; font-family: Poppins; font-size: 72px; padding: 20px 40px; background: #e11d48; border-radius: 16px; animation: pulse 1s ease-in-out infinite; } @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.18); } }",
    "animationDuration": 1
  }
}
```


```json
{
  "name": "docs-text-css-pulse",
  "width": 960,
  "height": 540,
  "duration": 5,
  "backgroundColor": "#140b2e",
  "visuals": [
    {
      "type": "TEXT",
      "html": "<div class=\"badge\">LIMITED OFFER</div>",
      "position": "center-center",
      "enterBegin": 0,
      "exitEnd": 5,
      "customCode": {
        "css": ".badge { color: #ffffff; font-family: Poppins; font-size: 72px; padding: 20px 40px; background: #e11d48; border-radius: 16px; animation: pulse 1s ease-in-out infinite; } @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.18); } }",
        "animationDuration": 1
      }
    }
  ]
}
```

The pulsing badge, rendered by Zvid

[Watch rendered example](https://cdn.zvid.io/library/docs/text-css-pulse.mp4)


### Example: JavaScript with the Web Animations API

Use `js` and `element.animate(...)` when you want to build motion
programmatically:

```json
{
  "type": "TEXT",
  "html": "<div class=\"price\">$9.99</div>",
  "position": "center-center",
  "enterBegin": 0,
  "exitEnd": 5,
  "customCode": {
    "css": ".price { color: #fbbf24; font-family: Poppins; font-size: 96px; font-weight: 700; }",
    "js": "document.querySelector('.price').animate([{ transform: 'translateY(0)' }, { transform: 'translateY(-30px)' }, { transform: 'translateY(0)' }], { duration: 800, iterations: Infinity, easing: 'ease-in-out' });"
  }
}
```


```json
{
  "name": "docs-text-waapi",
  "width": 960,
  "height": 540,
  "duration": 5,
  "backgroundColor": "#140b2e",
  "visuals": [
    {
      "type": "TEXT",
      "html": "<div class=\"price\">$9.99</div>",
      "position": "center-center",
      "enterBegin": 0,
      "exitEnd": 5,
      "customCode": {
        "css": ".price { color: #fbbf24; font-family: Poppins; font-size: 96px; font-weight: 700; }",
        "js": "document.querySelector('.price').animate([{ transform: 'translateY(0)' }, { transform: 'translateY(-30px)' }, { transform: 'translateY(0)' }], { duration: 800, iterations: Infinity, easing: 'ease-in-out' });"
      }
    }
  ]
}
```

Web Animations API motion, rendered by Zvid

[Watch rendered example](https://cdn.zvid.io/library/docs/text-waapi.mp4)


### Example: a pure-CSS spinner

Vector-looking graphics don't need markup at all — borders, gradients, and
border-radius go a long way:

```json
{
  "type": "TEXT",
  "html": "<div class=\"ring\"></div>",
  "width": 220,
  "height": 220,
  "position": "center-center",
  "enterBegin": 0,
  "exitEnd": 5,
  "customCode": {
    "css": ".ring { width: 160px; height: 160px; border-radius: 50%; border: 14px solid rgba(34,211,238,0.25); border-top-color: #22d3ee; animation: spin 1.5s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }",
    "animationDuration": 1.5
  }
}
```


```json
{
  "name": "docs-text-css-ring",
  "width": 960,
  "height": 540,
  "duration": 5,
  "backgroundColor": "#140b2e",
  "visuals": [
    {
      "type": "TEXT",
      "html": "<div class=\"ring\"></div>",
      "width": 220,
      "height": 220,
      "position": "center-center",
      "enterBegin": 0,
      "exitEnd": 5,
      "customCode": {
        "css": ".ring { width: 160px; height: 160px; border-radius: 50%; border: 14px solid rgba(34,211,238,0.25); border-top-color: #22d3ee; animation: spin 1.5s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }",
        "animationDuration": 1.5
      }
    }
  ]
}
```

A CSS-only loading ring, rendered by Zvid

[Watch rendered example](https://cdn.zvid.io/library/docs/text-css-ring.mp4)


### `customCode` safety

`customCode` runs inside the rendering browser, so it may **only** style, script,
and animate the element's own content. The renderer rejects anything that could
reach the network, filesystem, or browser storage, including:

- **JavaScript**: `fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource`,
  `sendBeacon`; `eval`, `new Function`, string-form `setTimeout`/`setInterval`,
  dynamic `import()`/`require`; `localStorage`/`sessionStorage`/`indexedDB`,
  `document.cookie`; service workers and Web Workers; `window.open` and
  navigation (`location = ...`); `document.write`.
- **CSS**: `@import`, `expression(...)`, `-moz-binding`, `javascript:` URLs, and
  `url(...)` pointing at any non-`data:` resource. Inline `url(data:...)` is
  allowed.

Keep your animations self-contained — define keyframes, target your own classes,
and use the Web Animations API for dynamic motion.

## Examples

These are element fragments for `payload.visuals`, not complete projects.
For video-only timing fields, use a video project with a long enough duration.

### Plain Text

```json
{
  "type": "TEXT",
  "text": "Hello World",
  "x": 640,
  "y": 360,
  "anchor": "center-center",
  "style": {
    "fontSize": 48,
    "color": "#000000",
    "textAlign": "center"
  }
}
```

### Styled Text With Animation

```json
{
  "type": "TEXT",
  "text": "Welcome to Zvid",
  "position": "center-center",
  "enterBegin": 0,
  "enterEnd": 1,
  "exitBegin": 9,
  "exitEnd": 10,
  "enterAnimation": "fade",
  "exitAnimation": "fade",
  "style": {
    "fontSize": 36,
    "color": "#ff6b35",
    "textAlign": "center",
    "fontWeight": "bold"
  }
}
```

### HTML With Inline Styles

```json
{
  "type": "TEXT",
  "html": "<div style=\"text-align:center\"><p style=\"color:#ff6b35; margin:0; font-size:48px\">Big Title</p><p style=\"color:#666666; font-size:18px\">Subtitle text</p></div>",
  "position": "center-center",
  "enterBegin": 0,
  "enterEnd": 2,
  "exitBegin": 8,
  "exitEnd": 10
}
```

### Watermark

```json
{
  "type": "TEXT",
  "text": "WATERMARK",
  "x": 660,
  "y": 340,
  "anchor": "center-center",
  "angle": 45,
  "opacity": 0.3,
  "track": 10,
  "style": {
    "fontSize": 72,
    "color": "#000000",
    "fontFamily": "Anton",
    "textAlign": "center"
  }
}
```

## Font Handling

Use exact Google Fonts family names, such as `Poppins`, `Montserrat`, `Roboto`,
`Open Sans`, `Lato`, `Playfair Display`, `Oswald`, or `Bebas Neue`. Set the
family in `style.fontFamily` (plain text / HTML) or in your `customCode.css`.

## Related Pages

- [Scenes](https://docs.zvid.io/docs/structure/scenes/)
- [Animation Effects](https://docs.zvid.io/docs/structure/animations/)
- [Image Elements](https://docs.zvid.io/docs/structure/image-elements/)
- [Video Elements](https://docs.zvid.io/docs/structure/video-elements/)
- [Anchor](https://docs.zvid.io/docs/structure/properties/anchor/)
