Authoring cards¶
The WebVTT file is both the script and the layout. Each cue's timing decides when its card appears, and its payload and cue settings decide what the card says and where it sits. A cue payload is either plain text or a JSON object.
How the parser reads a file¶
The player fetches the track and parses it with parseVtt() in src/vtt.js,
not with the browser's <track> renderer.
- The file is split into blocks on blank lines. Windows line endings and a leading byte-order mark are normalised.
- The
WEBVTTheader block is skipped.NOTE,STYLEandREGIONblocks are skipped entirely. - An optional cue identifier line (such as
step-1) may precede the timing line. It becomes the card'sid. - Timestamps are
MM:SS.mmmorHH:MM:SS.mmm. A comma is also accepted as the decimal separator. A cue with a malformed timestamp is dropped. - Every line after the timing line is the payload. A WebVTT cue cannot contain a blank line, so a payload is one run of non-empty lines.
Plain-text cards¶
00:02.000 --> 00:05.000 position:70%,line-right line:24%,start size:38%
Click the **Export** button in the toolbar.
Placement with cue settings¶
All values are percentages of the video box.
| Setting | Effect |
|---|---|
position:N%[,line-left\|center\|line-right] |
Horizontal pin. The optional position-align picks which edge or the centre of the card sits at that point. |
line:N%[,start\|center\|end] |
Vertical pin. The optional line-align picks the edge: start is the top, end is the bottom. |
size:N% |
Card width as a percentage of the video width. |
align:start\|center\|end |
Text alignment. It is also the default position-align when that is omitted. left and right are read as start and end. |
The position-align and line-align values combine into one of nine anchor
points. For example, line-right with end gives bottom-right, and center
with start gives top. When a cue has a position but no line, the card
sits at the bottom. When a cue has no placement settings at all, the card is
centred horizontally (x = 50 %) with its bottom edge at 88 % of the height.
The parser also reads vertical and region, but cards are always laid out
horizontally.
Worked example: position:70%,line-right line:24%,start size:38% pins the
card's top-right corner (line-right + start = top-right) at 70 % across
and 24 % down, and makes the card 38 % of the video width.
Markdown in card text¶
Card text is HTML-escaped first, so raw HTML never survives. Then a markdown subset is rendered:
- inline:
**bold**,*italic*,`code`,~~strike~~,[label](url); - block:
#to######headings,-/*/+bulleted lists,1.numbered lists, and---horizontal rules; - consecutive plain lines join into one paragraph with line breaks.
Links whose target starts with javascript:, data: or vbscript: are
replaced with #. A single plain line is rendered without a <p> wrapper.
JSON cards¶
A cue whose trimmed payload starts with { is parsed as JSON. Use it for
per-card styling or trusted HTML. If the JSON does not parse, the payload is
shown as plain text, so a typo never produces a blank card.
00:05.000 --> 00:08.000
{ "text": "Drag the slider to fan the cards out.",
"x": 8, "y": 62, "w": 36, "anchor": "top-left",
"bg": "#10243a", "fg": "#eaf2ff", "radius": 14, "padding": "14px 18px",
"shadow": "0 8px 30px rgba(0,0,0,.45)", "class": "tip" }
JSON fields¶
Every field is optional, but give one of text or html.
| Field | Type | Default | Notes |
|---|---|---|---|
text |
string | — | Plain text with the markdown subset. |
html |
string | — | Trusted HTML. Filtered only when the sanitize option is on. |
x |
number or string | derived | Horizontal anchor. A number is a percentage of the width; a string is a CSS length. |
y |
number or string | derived | Vertical anchor. A number is a percentage of the height. |
w |
number or string | auto |
Card width. |
anchor |
enum | bottom |
Which point of the card sits at (x, y): top-left, top, top-right, left, center, right, bottom-left, bottom, bottom-right. |
align |
enum | start |
Text alignment inside the card. |
bg |
string | theme | Card background: any CSS colour or gradient. |
fg |
string | theme | Text colour. |
font |
string | theme | CSS font shorthand or family. |
padding |
string | theme | CSS padding. |
radius |
number or string | theme | Border radius. |
border |
string | theme | CSS border. |
shadow |
string | theme | CSS box-shadow. |
opacity |
number | 1 |
Applied to a solid bg colour only. |
class |
string | — | Extra class names for your own CSS. |
enter / exit |
enum | fade |
Per-card transition: fade, slide-up, slide-down, none. |
Placement priority is: JSON x/y/w/anchor, then the cue's own
position/line/size, then the default (bottom-centre at y = 88 %). An
unknown anchor value falls back to bottom.
Security¶
Treat the WebVTT file as trusted content
Plain-text cards and JSON text cards are always HTML-escaped. A JSON
html card is inserted as-is. With sanitize: true the player removes
<script>, <style>, <iframe>, <object> and <embed> elements,
on* attributes, and href/src values starting with javascript:.
This is a convenience filter, not a hardened sanitiser: it does not cover
data: URLs, srcdoc, SVG <use>, formaction or CSS url(). For
untrusted input, sanitise upstream with a vetted library such as
DOMPurify.
Checking a track without a browser¶
The parser is pure JavaScript, so you can inspect a track under Node.js:
import { readFileSync } from "node:fs";
import { parseCards } from "vexy-vlip-js";
const cards = parseCards(readFileSync("demo.vtt", "utf8"));
for (const c of cards) console.log(c.start, c.end, c.placement.anchor, c.text);
parseCards() returns one card model per cue. See
API reference for the model's fields.