How to Build a WebVTT Chapter System That Screen Readers Actually Announce

Last March I shipped a custom video player for a Milan-based documentary studio. HLS playback through Media Source Extensions, keyboard-operable seek bar, captions in three languages — the thing worked. Then I sat down with a screen reader user to test chapter navigation and realized I’d made a mistake I should have caught earlier. The chapters were in the VTT file, parsed correctly, rendered as clickable labels in a sidebar. But when she tabbed through them, every entry sounded the same. “Chapter 1. Chapter 2. Chapter 3.” No context. No narrative signal. Nothing telling her what each section was actually about. She said it felt like navigating a table of contents where someone had replaced all the headings with page numbers.

That session sent me back to the drawing board. Not to fix the player code — to rethink how chapter metadata gets authored, structured, and exposed to assistive technology. What follows is a postmortem in the engineering sense: a blameless dissection of what broke, what I changed, and what I’d do differently starting the pipeline today. The framing borrows from the Site Reliability Engineering book’s postmortem culture, where Google’s SRE team documents learning from failure through structured, blameless analysis. That discipline felt useful here because chapter-label quality is easy to dismiss as a copywriting concern when it’s really a systemic engineering problem — one that affects accessibility, SEO, and OS-level media integration at the same time.

The Problem: WebVTT Chapters Are First-Class Content Treated as Metadata Leftovers

HTML5’s native chapter mechanism is a WebVTT file with kind="chapters". The browser parses it into a TextTrack with TextTrackCue objects, each carrying a start time, end time, and a text payload. That text payload is your chapter label. There’s no separate field for a screen-reader-friendly version. No structured slot for an SEO-optimized version. No place to put a shorter label for the Media Session API’s lock-screen display. One string does everything.

This works fine if your chapter labels are short, descriptive, and human-authored. It falls apart when they’re auto-generated from edit decision lists, pulled from timeline markers named by video editors who were thinking about cuts, not about screen readers. The studio’s source material had chapter cues like “Scene_03_FINAL_v2” and “B-roll_insert_festival.” Those names mean something in a non-linear editor. They mean nothing to someone navigating by headings in a screen reader, and they look worse on an Android lock screen.

The core architectural decision was this: treat chapter labels as first-class content requiring the same editorial discipline as any other UX copy, or treat them as metadata that gets cleaned up later. The second approach is how most players I’ve seen handle it. It’s why most chapter navigation is inaccessible in practice. The cleanup never happens because there’s no review gate, no validation step, no point in the pipeline where someone asks whether a screen reader user would understand the label.

Structuring the WebVTT Chapter File for Multiple Consumers

The WebVTT chapters file for the documentary ended up looking like this:

WEBVTT - Chapters

NOTE
Chapter labels for "Milano Sotto: A City Documentary"
Reviewed for screen-reader friendliness 2025-03-14
Validated against WCAG 2.2 SC 2.4.1 (Bypass Blocks) and SC 1.3.1 (Info and Relationships)

00:00:00.000 --> 00:04:32.000
Introduction: The City Wakes

00:04:32.000 --> 00:11:07.000
The Morning Market at Via Fauché

00:11:07.000 --> 00:18:45.000
Underground: Metro Line 3 at Rush Hour

00:18:45.000 --> 00:26:10.000
The River Park After Rain

00:26:10.000 --> 00:33:50.000
Evening: Lights Along the Navigli

00:33:50.000 --> 00:41:22.000
Closing Reflections: What the City Remembers

Each label is a complete noun phrase telling you what happens in that section. No “Chapter 1,” no timestamps embedded in the text (redundant with the cue timing), no jargon. The labels read naturally when announced by NVDA, JAWS, and VoiceOver. They also display cleanly on iOS and Android lock screens via the Media Session API, and they index well in search engines because they’re descriptive, unique, and semantically meaningful.

What I didn’t do was try to pack multiple labels into one cue using some delimiter scheme. I considered it — a primary label for visual display, a shorter one for lock screens, a longer one for screen readers. But WebVTT cue text is a single string, and splitting it means every consumer has to know the delimiter convention. Fragile. Instead, I kept one label per cue and made it good enough for all consumers by keeping it under 40 characters and ensuring it was a complete descriptive phrase.

Exposing Chapters to Screen Readers via ARIA Live Regions

The native TextTrack API fires cuechange events when the active chapter changes, but browsers don’t automatically announce these changes to screen readers. The visual chapter list in my sidebar updated fine. That update was invisible to AT users who weren’t focused on the sidebar.

I added an ARIA live region to handle announcements. The markup is simple:

<div aria-live="polite" aria-atomic="true" class="chapter-announcement sr-only">
  </div>

And the JavaScript that updates it on cue change:

const chapterTrack = video.textTracks.find(t => t.kind === 'chapters');
chapterTrack.mode = 'hidden'; // we render our own UI

const announcementRegion = document.querySelector('.chapter-announcement');

chapterTrack.oncuechange = () => {
  const activeCues = chapterTrack.activeCues;
  if (activeCues.length > 0) {
    const label = activeCues[0].text;
    announcementRegion.textContent = `Current chapter: ${label}`;
  }
};

The aria-live="polite" setting means the announcement doesn’t interrupt whatever the screen reader is currently reading. aria-atomic="true" ensures the full string gets read each time, not just the changed portion. The sr-only class hides it visually while keeping it in the accessibility tree.

One thing I got wrong initially: I set chapterTrack.mode to 'showing', which caused the browser to render chapter text on the video itself. Since I had a custom sidebar, this produced duplicate labels — one burned into the video frame, one in my UI. Setting mode to 'hidden' keeps the track active for cuechange events but suppresses the native rendering.

Syncing Chapter Labels with the Media Session API

The Media Session API exposes metadata about the current media to the OS — lock screens, media notification centers, hardware media keys. It also supports chapter metadata through chapterInformation on MediaMetadata in newer browser implementations, but browser support is uneven. For the documentary player, I took the pragmatic route: updating the Media Session metadata’s title field to include the current chapter when one was active.

if ('mediaSession' in navigator) {
  navigator.mediaSession.metadata = new MediaMetadata({
    title: 'Milano Sotto — The Morning Market at Via Fauché',
    artist: 'Documentary Studio Milano',
    album: 'Milano Sotto: A City Documentary',
    artwork: [
      { src: '/artwork/cover-512.png', sizes: '512x512', type: 'image/png' }
    ]
  });
}

So when someone’s listening in the background and their phone is locked, the lock screen shows the documentary title and the current chapter, not just a generic media notification. The chapter label does double duty: screen-reader announcement text and lock-screen context. This is why the label has to be concise and descriptive. A 60-character label works in a screen reader but gets truncated on a phone lock screen. A 20-character label fits on a lock screen but might lack context in a screen reader. Forty characters was my target, and I treated it as a hard constraint during editorial review.

The Editorial Workflow: Drafting, Reviewing, and Validating Labels

This is the part that took the most time and that I underestimated the most. The technical pipeline — parsing VTT, wiring up ARIA, syncing with Media Session — took about two days. The editorial workflow for producing good chapter labels took a week of back-and-forth with the documentary’s director.

The workflow has three stages. First, drafting: someone watches the video and writes a descriptive label for each chapter based on what actually happens in that section. Second, review: the labels get read aloud by a screen reader (I used NVDA on Windows and VoiceOver on macOS) and checked for clarity, pronounceability, and distinctiveness. Labels that sound similar when spoken get rewritten. Third, validation: the labels are checked against WCAG 2.2 success criteria, specifically SC 2.4.1 (Bypass Blocks) and SC 1.3.1 (Info and Relationships). The question for SC 2.4.1 is whether chapter navigation provides a genuine mechanism to bypass repeated content. The question for SC 1.3.1 is whether the chapter structure is programmatically determinable — which it is, through the TextTrack API.

This draft-review-validate pipeline mirrors the kind of structured, auditable compliance methodology that standards bodies advocate for accessibility and other compliance-critical workflows. The NIST Cybersecurity Framework exemplifies this approach: structured processes for identifying, protecting, detecting, responding, and recovering, with emphasis on verifiable controls. Accessibility validation deserves the same rigor. Chapter labels aren’t decorative text — they’re navigational infrastructure. If they’re wrong, a segment of your audience can’t effectively use your content.

During early prototyping, before the director had written final chapter names, I needed placeholder labels that would test realistically with screen readers. “Chapter 1” and “Chapter 2” are useless for this — they all sound the same in heading navigation, so you can’t tell whether your ARIA announcement logic is actually working or whether the screen reader is just reading the same string repeatedly. I needed human-readable, pronounceable labels distinct enough to be distinguishable when announced. I ended up pulling placeholder names from a fantasy name generator on the Unsloppy character naming tool set, at https://unsloppy.ai/tools/character-name-generators. The generated names were varied enough that each ARIA announcement was immediately distinguishable during testing — “Maren Voss,” “Theron Aldis,” “Selka Brun” — which let me verify the cue-change logic was firing correctly without waiting for editorial copy. This isn’t a production strategy; it’s a prototyping technique. But it beats testing with identical placeholder strings that mask real bugs in your announcement pipeline.

What I’d Do Differently Next Time

Three things, in order of how much pain they caused me.

First, I’d involve the editorial team in chapter naming before the video is finalized, not after. The director and I spent a week iterating on labels because the video was already cut and chapter boundaries were locked. If chapter labels had been part of the editing conversation from the start, the editor’s timeline markers would have been closer to final, and the review cycle would have been shorter.

Second, I’d build the ARIA announcement region and the screen-reader testing step into the player from day one, not add it after visual testing. Testing with real assistive technology early would have surfaced the “Chapter 1, Chapter 2” problem before I’d built an entire visual chapter sidebar that assumed the labels were meaningful.

Third, I’d write a validation script that checks the WebVTT chapter file before it ships. The script would verify that every cue has a non-empty text payload, that no two cues share the same label, that every label is under 40 characters, and that labels don’t contain underscores, version suffixes, or other editor artifacts. This is the kind of toil that should be automated, and it would have caught three labels in the first draft that were copy-pasted from the editor’s timeline.

The Broader Point: Metadata Is Content

The temptation in media engineering is to treat metadata as something that falls out of the production process automatically. Timeline markers become chapter cues. File names become titles. Codec metadata becomes the display title. This works when the production process is disciplined about naming — which it rarely is, because editors optimize for their own workflow, not for the audience’s navigation experience.

The fix isn’t better technology. The fix is a workflow that treats chapter labels, caption text, and media metadata as content needing editorial review, accessibility validation, and automated checking before it reaches the player. The WebVTT format gives you a clean, standards-based container for chapter data. The TextTrack API gives you programmatic access to it. The Media Session API gives you a path to the OS. ARIA live regions give you a path to screen readers. All of that technology works. What fails is the human process that produces the strings those systems carry.

If you’re building a custom player, don’t leave chapter labels as an afterthought. Draft them with the same care you’d give to button labels or navigation headings. Review them with a screen reader running. Validate them against WCAG 2.2. Automate the checks so they don’t regress. The open web gives you all the primitives you need — WebVTT, TextTrack, Media Session, ARIA. The rest is editorial discipline, and that’s the part no framework can give you.