---
title: "Native <track> Element Requires Blob URL, Not data: URI"
aliases: [track-element-blob-url, vtt-blob-url, track-data-uri-blocked]
tags: [browser, html, vtt, accessibility, react, javascript]
sources:
  - "daily/2026-04-29.md"
created: 2026-04-29
updated: 2026-05-01
---

# Native `<track>` Element Requires Blob URL, Not `data:` URI

Browsers block `data:text/vtt,...` URIs for the HTML `<track>` element due to same-origin/CSP restrictions. The `<track>` element only accepts `http://`, `https://`, or `blob:` URLs as its `src` attribute. To serve VTT captions dynamically from JavaScript (e.g., from an API response string), you must convert the content to a Blob URL using `URL.createObjectURL()` and clean it up in the component destructor with `URL.revokeObjectURL()`.

## Key Points

- `<track src="data:text/vtt;base64,...">` silently fails in all major browsers (no error, no captions)
- `<track src="data:text/vtt;charset=utf-8,...">` also blocked
- The fix is `URL.createObjectURL(new Blob([vttString], { type: "text/vtt" }))` → returns `blob:https://...`
- Always revoke the blob URL in a cleanup function to prevent memory leaks
- This also applies to WebVTT `.vtt` files fetched inline — if you have the string content, use Blob URL not data URI

## Details

### The Broken Pattern

```jsx
// ❌ BROKEN — browsers block data: URIs for <track>
function VideoPlayer({ vttContent }) {
  const dataUri = `data:text/vtt;charset=utf-8,${encodeURIComponent(vttContent)}`;
  return (
    <video controls>
      <source src="/video.mp4" type="video/mp4" />
      <track kind="subtitles" src={dataUri} srcLang="en" default />
    </video>
  );
}
// Result: no captions appear, no console error
```

### The Correct React Pattern

```jsx
import { useState, useEffect } from "react";

function VideoPlayer({ vttContent }) {
  const [trackUrl, setTrackUrl] = useState(null);

  useEffect(() => {
    if (!vttContent) return;

    // Create blob URL from VTT string
    const blob = new Blob([vttContent], { type: "text/vtt" });
    const url = URL.createObjectURL(blob);
    setTrackUrl(url);

    // Cleanup: revoke blob URL when component unmounts or vttContent changes
    return () => {
      URL.revokeObjectURL(url);
    };
  }, [vttContent]);

  return (
    <video controls>
      <source src="/video.mp4" type="video/mp4" />
      {trackUrl && (
        <track
          kind="subtitles"
          src={trackUrl}        // blob:https://... ✓
          srcLang="en"
          label="English"
          default
        />
      )}
    </video>
  );
}
```

### Vanilla JS Pattern

```javascript
function attachVttTrack(videoElement, vttString) {
  // Remove any existing track
  const existingTrack = videoElement.querySelector("track");
  if (existingTrack) {
    URL.revokeObjectURL(existingTrack.src);  // Cleanup old blob
    existingTrack.remove();
  }

  const blob = new Blob([vttString], { type: "text/vtt" });
  const url = URL.createObjectURL(blob);

  const track = document.createElement("track");
  track.kind = "subtitles";
  track.src = url;
  track.srclang = "en";
  track.default = true;
  videoElement.appendChild(track);

  // Return cleanup function
  return () => URL.revokeObjectURL(url);
}
```

### VTT Content Format Reminder

```
WEBVTT

00:00:00.000 --> 00:00:04.000
Hello, this is the first caption.

00:00:04.500 --> 00:00:08.000
And this is the second line.
```

The string must start with `WEBVTT` on the first line (no BOM, no extra whitespace before it).

## Gotchas

### `<track default>` + Custom JS Overlay = Double Captions

The `default` attribute on `<track>` tells the browser to **auto-activate** the text track, causing the browser's native caption renderer to display cues. If a custom JS overlay is also rendering captions from the same track, both renderers fire simultaneously and captions appear doubled.

```jsx
// ❌ BROKEN — native renderer + JS overlay both show captions
<track kind="subtitles" src={trackUrl} srcLang="en" default />

// ✅ CORRECT — remove 'default'; JS overlay is the sole renderer
<track kind="subtitles" src={trackUrl} srcLang="en" />
```

When using a custom caption overlay, always omit the `default` attribute and activate/read the track programmatically via the `TextTrack` API.

### VTT Cue Settings Are Not Auto-Applied to Custom Overlays

WebVTT cue settings (`line:`, `position:`, `size:`, `align:`) are instructions for the **browser's native renderer**. A custom JS overlay that reads `cue.text` does not automatically honour these settings — it receives the raw text string only.

```javascript
// cue.text = "Hello world"  — no position data here
// cue.line, cue.position, cue.align ARE accessible, but the overlay
// must explicitly read them and apply CSS positioning

track.addEventListener("cuechange", () => {
  const cues = track.activeCues;
  for (const cue of cues) {
    // Must read cue.line / cue.position and apply manually:
    overlayEl.style.top = cue.line !== "auto" ? `${cue.line}%` : "90%";
    overlayEl.textContent = cue.text;
  }
});
```

Silently ignoring cue settings while not stripping them causes subtle positioning bugs — text appears at the default position regardless of VTT metadata.

## Related Concepts

- [[wiki/tech-patterns/python-ai-agents]] — VTT is often generated by AI models (Gemini, Whisper) in accessibility pipelines
- [[wiki/concepts/export-endpoint-filter-pattern]] — related frontend/backend data delivery pattern

## Sources

- [[daily/2026-04-29.md]] — Encountered in Video Accessibility Platform when trying to render dynamically-fetched VTT captions in the browser player
