Skip to the content.

Intro · Source on GitHub

Feature support matrix

Legend: ✅ works as expected · ❌ does not work / not applied.

Every cell, including every iOS Safari cell, was independently confirmed per browser: screenshotted for rendering fixtures, DOM-dumped (TextTrack.cues/getCueAsHTML()) for timestamp parsing and API surface.

Styling cells reflect the default styling method, an in-file WEBVTT STYLE block, since that’s how a real, standalone .vtt file actually declares its own styling.

The iOS Safari column reflects Simulator testing (see the intro page).

API surface (VTTCue/VTTRegion feature detection)

Feature Chrome 151 Firefox 153 Safari 26.6 Safari TP 27.0 iOS Safari 26.5
VTTCue constructor + core props
VTTCue.lineAlign / .positionAlign ❌ missing
VTTCue.region ❌ missing (no VTTRegion)
VTTRegion constructor
<region> block parsing → cue.region ❌ (n/a)

Regions

Feature Chrome 151 Firefox 153 Safari 26.6 Safari TP 27.0 iOS Safari 26.5
Region layout (anchor/width) + multi-cue stacking ❌ falls back to normal cue
::cue-region() background styling (bare) n/a ❌ not applied ❌ not applied ❌ not applied ❌ not applied
::cue-region(#id) background styling (selector arg) n/a ❌ not applied ❌ not applied ❌ not applied ❌ not applied
Region layout overrides line/position/size/vertical n/a *

* Chrome has no regions to fall back to, so it applies these settings directly to the cue instead of ignoring them like the other three browsers do.

Markup tags & timestamps

Feature Chrome 151 Firefox 153 Safari 26.6 Safari TP 27.0 iOS Safari 26.5
<i> italic markup → native style
<b> bold markup → native style
<u> underline markup → native style
<b><i> nested markup
<ruby>/<rt> layout (annotation above base)
<c.class> class span
<lang> tag renders content
Internal timestamp tag parsing (<00:00:01.000>getCueAsHTML() timestamp node)
Short internal timestamp tag <MM:SS.mmm> (no hours)
Overlapping/simultaneous cue stacking (2 or 3 cues)

VTT cue settings

Feature Chrome 151 Firefox 153 Safari 26.6 Safari TP 27.0 iOS Safari 26.5
line / position / align / size (explicit %)
line:N (line-number snap-to-lines)
line:-N (negative line, count from bottom)
position:P%,line-left/line-right (positionAlign) ❌ entire setting dropped, falls back to auto *
line:P%,start/end (lineAlign) ❌ entire setting dropped, falls back to auto *
align:end
vertical:rl (rotated glyphs) ❌ renders horizontal
vertical:lr (rotated glyphs, opposite direction) ❌ renders horizontal
Text wraps inside a narrow size ❌ overflows/clips as one line, no wrap

* This syntax is in the current spec, but under live dispute: see “A few smaller things worth knowing” on the intro page for why Chrome dropped it.

::cue() selector types

Feature Chrome 151 Firefox 153 Safari 26.6 Safari TP 27.0 iOS Safari 26.5
::cue() bare (no argument), color
::cue() bare, background-color silently ignored
::cue(.class)
::cue(v[voice=...])
::cue(i) / ::cue(u) / ::cue(b) bare tag selectors
::cue(ruby) / ::cue(rt) independently styled
::cue(v) bare (no voice filter)
::cue(b.class) compound tag+class selector
::cue(v[voice=...].class) compound attr+class selector
::cue(#id) ✅ *
::cue(:lang(xx)) (unquoted ident)
::cue(:past) / ::cue(:future) on plain text (no element wrapping) ❌ †
::cue(c.class:past) / ::cue(*.class:past) on per-segment <c>-wrapped text
Same, but one element wraps the whole cue instead of one per segment ❌ no internal split (whole element renders one color) ❌ no internal split ❌ no internal split ❌ no internal split

* Renders as a full-width bar across the cue line, not a tight box like a native ::cue() match normally produces. Likely a different, newer code path, not the same feature simply re-enabled.

† Per spec, text nodes aren’t selectable at all. See the intro page’s karaoke section for the fix (wrap each segment in its own element).

CSS properties on ::cue()

Property Chrome 151 Firefox 153 Safari 26.6 Safari TP 27.0 iOS Safari 26.5
color
background-color (with an argument selector)
background-image ❌ * ❌ * ❌ *
font shorthand (style/weight/size/family)
line-height ⚠️ not observable (blocked by the no-wrap bug above)
opacity
outline
text-decoration (incl. wavy/color)
text-shadow
visibility: hidden
white-space: pre (preserves runs of spaces)
ruby-position ❌ †
text-combine-upright (in vertical:rl text)

* Fails via the default STYLE-block method in Firefox, Safari, and Safari TP — Chrome is the only browser where STYLE-block background-image works at all. Works fine everywhere via the secondary page-level-<style>/external-stylesheet methods, tiling the checkerboard correctly — the only property where those methods diverge from the default. The animated-formats tables below confirm and extend this same fact across GIF/APNG/WebP/SVG and a bare (class-free) selector.

† Chrome supports this property generally outside WebVTT, since Chrome 84. It’s specifically inert inside ::cue()’s permitted-property set.

Every selector type and property above was also checked via those two secondary methods, and matched the default STYLE-block result exactly, except the one background-image row.

Animated background-image formats

Whether an animated background shows up — and whether it actually animates — depends as much on how the image is referenced as on its format.

Network-referenced image (a URL, not a data URI), via page-level <style> — the only method that reaches Firefox/Safari/Safari TP at all, since none of them fetch network images from inside a STYLE block (same restriction as the background-image row above):

Format Chrome 151 Firefox 153 Safari 26.6 Safari TP 27.0
Animated GIF ✅ animates ✅ animates * ✅ animates ✅ animates
Animated PNG (APNG) ✅ animates ✅ animates * ✅ animates ✅ animates
Animated WebP ✅ animates ✅ animates * ✅ animates ✅ animates
SVG animated via SMIL (<animate>) ❌ frozen ❌ frozen * ❌ frozen ❌ frozen
SVG animated via CSS @keyframes ❌ frozen ❌ frozen * ❌ frozen ❌ frozen

Base64 data URI, in the .vtt file’s own STYLE block (i.e. the one way any of this could reach a real, self-contained subtitle file’s default styling method) — this is the same Chrome-only STYLE-block restriction as the background-image row in the CSS-properties table above, now confirmed across every format here and with a bare, class-free selector:

Format Chrome 151 Firefox 153 Safari 26.6 Safari TP 27.0
Animated GIF ✅ animates ❌ † ❌ † ❌ †
Animated PNG (APNG) ✅ animates ❌ † ❌ † ❌ †
Animated WebP ✅ animates ❌ † ❌ † ❌ †
SVG animated via SMIL (<animate>) ✅ animates ❌ † ❌ † ❌ †
SVG animated via CSS @keyframes ✅ animates ❌ † ❌ † ❌ †

The SVG rows are the interesting reversal: a network-loaded SVG’s own animation never plays anywhere (treated as a fully static image, like <img src="*.svg">), but the identical SVG base64-encoded animates normally — in all four browsers, once it’s actually reachable (page-level <style>, since STYLE block only reaches Chrome). Encoding, not format, is what determines whether an SVG’s own animation runs.

* Firefox can’t reach any of these through ::cue(.class) at all (same argument-selector failure as the selector-types table above) — verified separately with a bare, class-free ::cue{} rule instead, where the underlying animation behavior turns out identical to the other three browsers.

† Same fact as the background-image row’s footnote in the CSS-properties table above: STYLE-block background-image only works in Chrome, base64 or not, any format — confirmed by screenshot here for GIF/APNG/WebP/SVG, not just the one property tested there.

Not independently re-tested on iOS Safari for either table on this page.

Parsing edge cases

Feature Chrome 151 Firefox 153 Safari 26.6 Safari TP 27.0 iOS Safari 26.5
Page-level <style> / external stylesheet match the default STYLE-block method ✅ identical ✅ identical ⚠️ except background-image ⚠️ except background-image ✅ identical
NOTE block ignored / never rendered
NOTE block swallows an adjacent non-blank line (no blank-line terminator) ✅ (cue id dropped, per spec) ❌ (terminates NOTE early, next line wrongly parsed as cue id)
BOM-prefixed file parses
Short cue timing MM:SS.mmm (no hours)
HTML character references (&amp; &lt; &gt; &nbsp; &lrm;) decode correctly in getCueAsHTML()

video.js

video.js 8.23.9 implements none of the WebVTT CSS styling spec: no ::cue(), no ::cue-region(), no shadow DOM. It paints cues itself as plain HTML. A developer can reverse-engineer its internal DOM and write different, non-standard CSS that visually approximates a ::cue() rule, but an in-file STYLE block (this report’s default styling method for native browsers) parses fine and is then silently ignored, same as page-level CSS.

What is comparable is the WebVTT parsing/cue-settings model video.js implements independently of any CSS:

Feature video.js 8.23.9
Cue-settings layout (line/position/align/size, positionAlign/lineAlign comma syntax, vertical:rl/lr, narrow-size wrapping) ✅ matches Firefox/Safari/STP, better than Chrome (see above)
<region> / VTTRegion support ❌ none at all (same fallback as Chrome)
:past/:future karaoke highlighting ❌ not implemented, cues never re-render on timeupdate
Styling one specific cue by its .id ❌ impossible, id never reaches the DOM at all
Short MM:SS.mmm cue-level timing (no hours) ❌ cue silently never appears, genuine parser bug
Internal short timestamp <MM:SS.mmm>
HTML entities incl. double-escaping edge case
Overlapping/simultaneous cue stacking