A TypeScript library for parsing, building, and manipulating WebVTT subtitle files. Supports both VTT and SRT formats.
npm install js-vtt
# or
pnpm add js-vtt
# or
yarn add js-vttRequires an ESM environment. The package ships as pure ESM (
"type": "module").
import { VTT } from 'js-vtt';
const vtt = new VTT().addCue(0, 3.5, 'Hello, world!').addCue(4, 7, 'This is a subtitle.');
console.log(vtt.toString());
// WEBVTT
//
// 00:00:00.000 --> 00:00:03.500
// Hello, world!
//
// 00:00:04.000 --> 00:00:07.000
// This is a subtitle.- Parsing
- Building
- Segments
- Querying segments
- Timing utilities
- Validation
- Serialization
- Browser utilities
- Errors
Parses a WebVTT string. Unrecognized blocks throw InvalidVttError. A missing or malformed header throws InvalidHeaderError.
import { VTT } from 'js-vtt';
const raw = `WEBVTT
00:00:01.000 --> 00:00:04.000
Hello!`;
const vtt = VTT.fromString(raw);Fetches the file at the given URL and parses it. Auto-detects SRT vs VTT. Non-OK HTTP responses and network failures reject the promise; parse errors are rethrown as typed errors.
const vtt = await VTT.fromURL('https://example.com/subtitles.vtt');Parses a browser File object. Auto-detects SRT vs VTT.
const vtt = await VTT.fromFile(file); // file: FileConverts an SRT-formatted string to a VTT instance. Cue sequence numbers are preserved as cue identifiers.
const srt = `1
00:00:01,000 --> 00:00:04,000
Hello!`;
const vtt = VTT.fromSRT(srt);Reconstructs a VTT instance from the output of vtt.toJSON().
const json = vtt.toJSON();
const restored = VTT.fromJSON(json);Combines two or more VTT instances into one. The header of the first instance is used. Segments are deep-cloned so mutating the result does not affect the sources.
const merged = VTT.merge(vttA, vttB, vttC);All builder methods return this, so they can be chained.
import { VTT } from 'js-vtt';
const vtt = new VTT('My subtitles')
.addCue(0, 2, 'First line')
.addCue(3, 6, 'Second line', 'cue-2')
.addComment('Translated by Jane')
.addStyle(['::cue'], { color: 'yellow' });| Parameter | Type | Description |
|---|---|---|
description |
string (optional) |
Text placed on the WEBVTT header line |
meta |
Record<string, string> (optional) |
Key-value metadata appended below the header line |
const vtt = new VTT('Example', { Kind: 'captions', Language: 'en' });
// Produces: WEBVTT Example
// Kind: captions
// Language: enUpdates the header after construction.
vtt.setHeader('Updated title', { Language: 'fr' });The header is created automatically when you construct a VTT instance. Use setHeader() to update it, or access it via the header getter.
vtt.header.description; // string | undefined
vtt.header.meta; // Record<string, string>A cue is a timed block of subtitle text.
| Parameter | Type | Description |
|---|---|---|
startTime |
number |
Start time in seconds |
endTime |
number |
End time in seconds |
text |
string |
Subtitle text (may contain VTT markup tags) |
identifier |
string | number (optional) |
Cue identifier |
settings |
CueSettings (optional) |
Positioning and layout settings |
vtt.addCue(10, 15, '<b>Bold text</b>', 'intro', {
align: 'center',
position: '50%',
size: '80%',
});type CueSettings = {
vertical?: 'rl' | 'lr'; // vertical text direction
line?: number | string; // vertical position
position?: string; // horizontal position (e.g. "50%")
size?: string; // cue box width (e.g. "80%")
align?: 'start' | 'center' | 'end';
region?: string; // ID of a REGION block
};import { Cue } from 'js-vtt';
const cue = new Cue(0, 5, 'Hello');
cue.setStartTime(1).setEndTime(6).setText('Hi there').setIdentifier('c1');
// Remove inline HTML/VTT tags from the text
cue.removeTags();
// Parse from a raw VTT block string
const parsed = Cue.fromString('00:00:01.000 --> 00:00:04.000\nHello!');Regions define named rectangular areas on screen for cue positioning.
| Parameter | Type | Description |
|---|---|---|
id |
string (optional) |
Region identifier, referenced by cue settings |
width |
number (optional) |
Width as a percentage (e.g. 40 for 40%) |
lines |
number (optional) |
Number of lines tall |
regionAnchor |
[number, number] (optional) |
[x%, y%] anchor point within the region |
viewportAnchor |
[number, number] (optional) |
[x%, y%] anchor point within the viewport |
scroll |
'up' (optional) |
Scroll direction |
vtt.addRegion('bottom', 40, 3, [0, 100], [10, 90], 'up');import { Region } from 'js-vtt';
const region = new Region();
region
.setId('top')
.setWidth(80)
.setLines(2)
.setRegionAnchor([50, 100])
.setViewportAnchor([50, 90])
.setScroll('up');Style blocks embed CSS that targets cue elements.
| Parameter | Type | Description |
|---|---|---|
selectors |
string[] |
CSS selectors (e.g. ['::cue']) |
declarations |
Partial<Pick<CSSStyleDeclaration, CueCSSProperty>> |
Only WebVTT-supported CSS properties are accepted |
vtt.addStyle(['::cue'], { color: 'white', backgroundColor: 'rgba(0,0,0,0.8)' });
vtt.addStyle(['::cue(b)'], { fontWeight: 'bold', color: 'yellow' });Supported CSS properties include: color, opacity, visibility, background, backgroundColor, font, fontSize, fontFamily, fontWeight, fontStyle, fontVariant, fontStretch, textDecoration, textShadow, outline, lineHeight, whiteSpace, textCombineUpright, rubyPosition, and their sub-properties.
import { Style } from 'js-vtt';
const style = new Style([{ selectors: ['::cue'], declarations: { color: 'white' } }]);
style.addRule({ selectors: ['::cue(b)'], declarations: { fontWeight: 'bold' } });
style.removeRule(0); // remove rule at index 0Comment blocks (NOTE) are included in VTT output but stripped when serializing to SRT.
vtt.addComment('This file was generated by js-vtt');import { Comment } from 'js-vtt';
const comment = new Comment('Translator notes go here');
comment.setText('Updated notes');Adds any Segment subclass instance directly, for cases where you construct segments manually.
import { Cue, Region } from 'js-vtt';
vtt.addSegment(new Cue(0, 1, 'Hi')).addSegment(new Region('r1'));Returns all Cue segments.
const cues = vtt.getCues(); // Cue[]Returns all cues whose time range overlaps [start, end] (in seconds). Useful for rendering cues at a given playback position.
const visible = vtt.getCuesByTime(5, 10); // cues active between 5s and 10sReturns the first cue matching the given identifier, or undefined.
const cue = vtt.getCueById('intro');Returns all segments of a given type. Accepts either a class constructor or a segment type string.
import { Cue, Region } from 'js-vtt';
vtt.getSegmentsByType(Cue); // Cue[]
vtt.getSegmentsByType(Region); // Region[]
vtt.getSegmentsByType('style'); // Style[]A shallow copy of all segments including the header (index 0). Mutating the returned array does not change the VTT instance.
vtt.segments; // Segment[]All timing methods return this for chaining.
Shifts all cue timings by offset seconds. Use a positive value to delay, negative to advance.
vtt.shiftTime(5); // delay all cues by 5 seconds
vtt.shiftTime(-2); // advance all cues by 2 secondsScales all cue timings proportionally when the video length has changed.
// Video was 120 s, now re-encoded to 118 s
vtt.rescale(120, 118);Remaps cue timings when the video frame rate has changed. Every timestamp is multiplied by sourceFps / targetFps.
// Subtitles were authored for 59.94 fps video, target is 29.97 fps
vtt.syncFps(59.94, 29.97);Returns true if the header and all segments are structurally valid per the WebVTT spec.
if (!vtt.validate()) {
console.error('VTT file has invalid segments');
}Returns an array of { index, segment } objects for every invalid segment. An empty array means the file is valid. More useful than validate() when you need to know what is wrong.
const errors = vtt.getValidationErrors();
// [{ index: 2, segment: Cue { ... } }, ...]
for (const { index, segment } of errors) {
console.error(`Segment at index ${index} is invalid:`, segment.toJSON());
}Every segment exposes a valid getter that returns true if the segment is structurally valid.
const cue = new Cue(5, 3, 'Bad'); // endTime < startTime
cue.valid; // falseisValid() is also available as a backwards-compatible alias for valid.
Serializes the VTT instance to a string.
format |
Output |
|---|---|
'vtt' |
WebVTT format (default) |
'srt' |
SubRip (SRT) format |
// WebVTT
const vttString = vtt.toString();
const vttString2 = vtt.toString('vtt');
// SRT (NOTE and STYLE blocks are omitted; inline tags stripped from cue text)
const srtString = vtt.toString('srt');Returns a plain object representing the full VTT file. Each segment includes a _type discriminant field ('cue', 'region', 'style', 'comment', 'header').
const json = vtt.toJSON();
// {
// header: { _type: 'header', description: '...', meta: { ... } },
// segments: [
// { _type: 'cue', startTime: 0, endTime: 5, text: '...', ... },
// ...
// ]
// }Individual segments also have toString() and toJSON() methods.
Creates a TextTrack on the given HTMLVideoElement and populates it with VTTCue objects from all cues in the instance. Cue identifiers and settings (align, line, position, size, vertical) are copied onto each VTTCue when present.
const track = vtt.attachToVideo(videoEl, 'subtitles', 'English', 'en');
track.mode = 'showing';| Parameter | Type | Description |
|---|---|---|
video |
HTMLVideoElement |
The video element to attach the track to |
kind |
TextTrackKind |
e.g. 'subtitles', 'captions', 'descriptions' |
label |
string (optional) |
Human-readable track label |
language |
string (optional) |
BCP 47 language tag (e.g. 'en', 'fr') |
All errors extend the native Error class and include the offending segment string in the message for easy debugging.
| Class | Thrown when |
|---|---|
InvalidHeaderError |
VTT.fromString() receives a missing/malformed WEBVTT header |
InvalidCueError |
Cue.fromString() receives a malformed cue block |
InvalidRegionError |
Region.fromString() receives a malformed region block |
InvalidStyleError |
Style.fromString() receives a malformed style block |
InvalidCommentError |
Comment.fromString() receives a malformed NOTE block |
InvalidVttError |
Unrecognized blocks, invalid structure, or bad timing-utility arguments |
SrtValidationError |
VTT.fromSRT() receives a segment without a valid timing line |
import { VTT, InvalidHeaderError } from 'js-vtt';
try {
VTT.fromString('not a vtt file');
} catch (e) {
if (e instanceof InvalidHeaderError) {
console.error('Bad header:', e.message);
}
}