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
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.
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.
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[]The full list of all segments including the header (index 0).
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.
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 |
General VTT-level validation failure (parsing or structure) |
SrtValidationError |
SRT-specific validation failure |
import { VTT, InvalidVttError } from 'js-vtt';
try {
VTT.fromString('not a vtt file');
} catch (e) {
if (e instanceof InvalidVttError) {
console.error('Bad VTT:', e.message);
}
}