What Is a VTT File? The WebVTT Format, an Example, and What Browsers Actually Show
A VTT file (.vtt, short for WebVTT, Web Video Text Tracks) is the subtitle format of web video. Like an SRT file, it's plain text: a list of cues, each with a start time, an end time and the words to show. On top of that
A VTT file (.vtt, short for WebVTT, Web Video Text Tracks) is the subtitle format of web video. Like an SRT file, it's plain text: a list of cues, each with a start time, an end time and the words to show. On top of that it can carry positioning, styling and comments. It's the only subtitle format the HTML5 <track> element accepts, so any video player in a web page reads it.
Most explanations stop at "VTT supports styling and positioning". We wanted to know which of those extras actually show up, so we wrote one test file per feature and played each one in Chrome 155 and Safari 26.2, and five of them on an iPhone. Then we broke VTT files in 18 common ways and fed them to both browsers, to ffmpeg and to our own converter. The results are below, after the format itself.
What a VTT file looks like
These are the first three cues of a real file: subtitles ScribeToAny made for the 64-second bakery interview from our tutorial. They're the same cues as in our SRT example, exported as VTT.
WEBVTT
00:00:00.000 --> 00:00:01.680
Welcome back to Small Shop Stories.
00:00:02.160 --> 00:00:05.320
Today I'm talking to Maya, who runs a tiny bakery on the
00:00:05.320 --> 00:00:06.320
corner of Mill Street.
A VTT file has two kinds of block, separated by blank lines:
-
The header. The file must start with the word
WEBVTT, in capitals. Text may follow it on the same line after a space (WEBVTT - Kitchen interview), and then comes a blank line. -
Cues. Each cue is an optional identifier line, then a timing line, then one or more lines of text. The timing line is
start --> end, with a dot before the milliseconds:00:00:05.320is 5.32 seconds. Hours are optional, so00:05.320is valid too. Anything after the end time on the same line is a cue setting, such asline:0.
The file also has to be UTF-8. There's no other encoding option.
Three more block types can sit between cues: NOTE (a comment that is never displayed), STYLE (CSS for the cues) and REGION (a named area of the screen for cues to scroll in).
VTT vs SRT
The cue text is usually identical, which is why converting between the two is lossless for plain subtitles. The differences are in the frame around it:
| VTT | SRT | |
|---|---|---|
| First line |
WEBVTT (required) |
Cue number 1
|
| Millisecond separator | Dot: 00:00:05.320
|
Comma: 00:00:05,320
|
| Cue numbers | Optional identifiers, any text | Expected, 1, 2, 3β¦ |
| Hours in timestamps | Optional | Required |
| Positioning | Cue settings (line, position, align, size, vertical) |
Not in the format |
| Styling |
<b>, <i>, <u>, classes, voices, CSS in STYLE blocks |
<b>, <i>, <u>, <font> in many players |
| Comments |
NOTE blocks |
None |
| Plays in a web page | Yes, via <track>
|
No |
| Specification | W3C | None |
In short: use VTT when the video plays in a browser, and SRT when it goes to an editor, a TV or a desktop player. For the rest of the formats, see subtitle and transcript file formats.
What browsers actually show: we tested 17 VTT features
We wrote one small VTT file per feature and loaded each into a <video> element with a <track>, served from a local web server. Each test seeked to a point where the cue was on screen, took a screenshot of the video, and read back what the browser had parsed through the page's textTracks API. Chrome ran headless on a fresh profile, and we repeated the odd results with a visible window; Safari was driven through safaridriver. Both on macOS 26.2.
| Feature | Chrome 155 | Safari 26.2 |
|---|---|---|
| Plain cue, NOTE block, cue identifier, hour-less timestamps | β | β |
line:0 (top), line:50% (middle) |
β | β |
position:10% align:start size:40% |
β | β (wraps in a narrower box) |
vertical:rl (vertical text) |
β | β |
<b>, <i>, <u>
|
β | β |
STYLE block: ::cue colour, font and size |
β | β |
Classes: <c.yellow> styled by ::cue(.yellow)
|
β | β |
Voice: <v Maya> styled by ::cue(v[voice="Maya"])
|
β (name not shown) | β (name not shown) |
Character references (&, <, ) |
β | β |
<lang ja> around Japanese and Chinese text |
β | β |
<ruby> reading over kanji |
β | β οΈ reading on its own line above |
Karaoke timestamps styled by ::cue(:past) / :future
|
β no colour change | β no colour change |
REGION with lines:3 and scroll:up
|
β ignored | β οΈ region drawn, fourth line never scrolled in |
| Four overlapping cues, after seeking into them | β οΈ only the first shown | β all four |
What the feature test shows
The everyday features are safe. Placement, bold and italics, colours and fonts from a STYLE block, classes and voice styling all rendered the same in both browsers. If you need a caption at the top of the frame so it doesn't cover a lower third, line:0 works.
The iPhone matched the Mac. We played five of the test files in Safari on an iPhone, both inline and in fullscreen: positioning, the STYLE block's colour and font, classes, ruby and four overlapping cues. Fullscreen kept the styling, and ruby showed the same separate reading line as Safari on the Mac.
Regions and karaoke aren't ready. Chrome dropped the region entirely: its textTracks API reported no region on any cue, and the cues stacked at the bottom like ordinary ones. Neither browser coloured the karaoke words, whether the video was paused or playing.
Overlapping cues break in Chrome after a seek. This one surprised us. When two people talk at once, a subtitle file often has cues whose times overlap. We played four overlapping cues from the start, and Chrome showed all four. But when we jumped into the middle of them, as a viewer does when they drag the progress bar, Chrome showed only the cue that started first, and its activeCues list agreed. We saw the same thing with just two overlapping cues and with cues that end at different times. Safari showed every cue in every case. If your captions overlap, test them in Chrome with some seeking, or split the overlap into separate, consecutive cues.
YouTube is stricter still. YouTube's help page lists WebVTT as an "advanced" format: positioning is supported, but styling is limited to <b>, <i> and <u>. Classes and STYLE blocks won't survive an upload there.
Why your VTT subtitles don't appear: we broke them 18 ways
A file that fails loudly gets fixed. The dangerous failures are the ones where the video plays, nothing warns you, and the captions just aren't there. We took a valid two-cue file, broke it in 18 ways, and ran each version through Chrome, Safari, ffmpeg 8.1.1 (converting to SRT) and ScribeToAny's subtitle converter, the parser behind our free VTT tools.
"Error" means the browser fired the track's error event, which a developer can at least catch. "Silent" means the track loaded fine with nothing in it, or something was lost without a warning.
| What's wrong with the file | Chrome 155 | Safari 26.2 | ffmpeg 8.1.1 | ScribeToAny |
|---|---|---|---|---|
| Nothing (valid file) | β | β | β | β |
| Byte-order mark at the start | β | β | β | β |
| Windows line endings (CRLF) | β | β | β | β |
WEBVTT - title header |
β | β | β | β |
| No spaces around the arrow | β | β | β | β |
One-digit hours (0:00:05.000) |
β | β | β | β |
| No blank line between cues | β | β | β | β |
No WEBVTT header |
β Error | β Error | β (with a warning) | β |
Lowercase webvtt header |
β Error | β Error | β Nothing read | β |
An SRT file renamed to .vtt
|
β Error | β Error | β Read as SRT | β |
| Comma before the milliseconds | β Silent: 0 cues | β Silent: 0 cues | β Nothing read | β |
Two-digit milliseconds (05.32) |
β Silent: 0 cues | β Silent: 0 cues | β | β |
No blank line after WEBVTT
|
β | β | β οΈ Silent: first cue lost | β |
| Blank line inside a cue's text | β οΈ Silent: text after it lost | β οΈ Silent: text after it lost | β οΈ Silent: next cue lost too | β οΈ Same as browsers |
| End time before start time | β οΈ Cue never shows | β οΈ Cue never shows | β End written as 1193:03:02,296
|
β οΈ Kept; flagged by the editor's quality check |
| Saved as Latin-1 ("cafΓ©") | β οΈ Garbled: cafοΏ½
|
β οΈ Garbled: cafοΏ½
|
β οΈ Silent: cue dropped | β Encoding detected |
SRT tags <font color>, {\an8}
|
{\an8} shown as text |
{\an8} shown as text |
{\an8} mangled |
{\an8} removed |
Served as text/plain, not text/vtt
|
β | β | n/a | n/a |
| On another domain, no CORS headers | β Error | β Plays | n/a | n/a |
What the broken-file test shows
A comma in the timestamps is the classic trap. It's what you get when you rename an SRT, or fix its header but not its times. Both browsers accepted the file, reported that it loaded, and showed no subtitles at all. Two-digit milliseconds did the same. If a VTT track "loads" but nothing appears, check the timing lines first: 00:00:05.320, with a dot and three digits.
Browsers are strict about the header, and lenient about almost everything else. No header, or a lowercase one, and the track fails. A missing blank line between cues, a BOM, CRLF line endings, one-digit hours and a text/plain content type were all fine.
Cross-origin files behave differently by browser. Served from another domain without CORS headers, the same valid file played in Safari and failed in Chrome. So a page can work on the Mac you tested on and show no captions for Chrome users. Serve the .vtt from the same domain as the page, or send an Access-Control-Allow-Origin header and add crossorigin to the <video> element. With both, Chrome loaded it.
Converting a VTT can quietly change it. ffmpeg turned an end time that came before its start time into 1193:03:02,296 in the SRT it wrote, dropped the first cue when the header had no blank line after it, and dropped a whole cue after a stray blank line. Check the output's cue count against the input's.
Our own converter had a VTT bug, and the test caught it. It read every broken file above, but it kept VTT-only markup in the text. Converting the voice-tag test file to TXT gave you <v Maya>Voice tagβ¦</v>, and & came out as & instead of &. Karaoke timestamps and <ruby> readings leaked the same way, into TXT, Word, PDF and SRT output. We fixed it the same day: VTT cue text now loses the tags that only a browser understands (<c>, <v>, <lang>, <ruby> readings, karaoke timestamps), keeps <b>, <i> and <u> because SRT has them too, and decodes character references. The cases are in our regression checks.
What we didn't test: two desktop browsers on one Mac, plus one ffmpeg build, with small synthetic files. Firefox, Android, smart TVs and video players such as VLC each have their own WebVTT code and may behave differently. On the iPhone we checked five features, not all 17, and didn't test seeking.
How to open, make and convert a VTT file
- To read it: any plain-text editor, such as Notepad or TextEdit.
-
To watch it: most desktop players load it next to the video. In a web page, add it with
<track kind="subtitles" srclang="en" src="file.vtt">inside the<video>element. Usekind="captions"for captions that include sound descriptions; the difference is explained in closed captions vs subtitles. - To make one from a recording: upload the audio or video to the subtitle generator and export VTT.
- To convert: SRT to VTT adds the header and swaps the commas for dots. VTT to SRT goes the other way, and VTT to text keeps only the words. All three run in your browser, so the file isn't uploaded.
- To fix one: open it in the SRT editor, which reads VTT too and flags cues that end before they start or go by too fast to read. Then save a clean copy.
FAQ
What's the MIME type for VTT? text/vtt. Set it on your server anyway, even though Chrome 155 and Safari 26.2 also played a file sent as text/plain in our test.
Is VTT better than SRT? For web video, yes, because browsers only accept VTT. For everything else, SRT is the safer default: more editors, TVs and upload forms take it.
Can I just rename an .srt to .vtt? No. Both browsers rejected the renamed file. You need the WEBVTT header and dots instead of commas, which is what the SRT to VTT converter does.
A VTT file is an SRT file with a header, dots for commas and room for extras. The basics work everywhere we tested. The extras depend on the browser, and the worst failures are silent: a comma in a timestamp, a file on another domain, a seek into overlapping cues. Keep the timing lines exact, serve the file from your own domain, and play it in more than one browser before it ships.
Originally published on the ScribeToAny blog.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.