diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index d863290d..a4630817 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -30,6 +30,7 @@ export {
// UI
export { AlertDialog, type AlertDialogContextValue, useAlertDialogContext } from './ui/alert-dialog';
export { BufferingIndicator, type BufferingIndicatorProps } from './ui/buffering-indicator/buffering-indicator';
+export { CaptionsButton, type CaptionsButtonProps } from './ui/captions-button/captions-button';
export { Controls } from './ui/controls';
export type { ControlsGroupProps } from './ui/controls/controls-group';
export type { ControlsRootProps } from './ui/controls/controls-root';
diff --git a/site/public/docs/demos/captions-button/captions.vtt b/site/public/docs/demos/captions-button/captions.vtt
new file mode 100644
index 00000000..6c725cad
--- /dev/null
+++ b/site/public/docs/demos/captions-button/captions.vtt
@@ -0,0 +1,10 @@
+WEBVTT
+
+00:00.000 --> 00:03.000
+This is a demo of the captions button.
+
+00:03.000 --> 00:06.000
+Captions help make video accessible to everyone.
+
+00:06.000 --> 00:09.000
+Toggle captions on and off with a single click.
diff --git a/site/src/components/docs/demos/captions-button/html/css/BasicUsage.astro b/site/src/components/docs/demos/captions-button/html/css/BasicUsage.astro
new file mode 100644
index 00000000..bf311233
--- /dev/null
+++ b/site/src/components/docs/demos/captions-button/html/css/BasicUsage.astro
@@ -0,0 +1,10 @@
+---
+import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro';
+import html from './BasicUsage.html?raw';
+import './BasicUsage.css';
+---
+
+
+
diff --git a/site/src/components/docs/demos/captions-button/html/css/BasicUsage.css b/site/src/components/docs/demos/captions-button/html/css/BasicUsage.css
new file mode 100644
index 00000000..0cb25055
--- /dev/null
+++ b/site/src/components/docs/demos/captions-button/html/css/BasicUsage.css
@@ -0,0 +1,34 @@
+.html-captions-button-basic {
+ position: relative;
+}
+
+.html-captions-button-basic video {
+ width: 100%;
+}
+
+.html-captions-button-basic__button {
+ padding-block: 8px;
+ position: absolute;
+ bottom: 10px;
+ left: 10px;
+ background: rgba(255, 255, 255, 0.7);
+ backdrop-filter: blur(10px);
+ color: black;
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ border-radius: 9999px;
+ padding-inline: 20px;
+ cursor: pointer;
+}
+
+.html-captions-button-basic__button .show-when-active {
+ display: none;
+}
+.html-captions-button-basic__button .show-when-inactive {
+ display: none;
+}
+.html-captions-button-basic__button[data-active] .show-when-active {
+ display: inline;
+}
+.html-captions-button-basic__button:not([data-active]) .show-when-inactive {
+ display: inline;
+}
diff --git a/site/src/components/docs/demos/captions-button/html/css/BasicUsage.html b/site/src/components/docs/demos/captions-button/html/css/BasicUsage.html
new file mode 100644
index 00000000..855afa10
--- /dev/null
+++ b/site/src/components/docs/demos/captions-button/html/css/BasicUsage.html
@@ -0,0 +1,17 @@
+
+
+
+
+ Captions Off
+ Captions On
+
+
+
diff --git a/site/src/components/docs/demos/captions-button/html/css/BasicUsage.ts b/site/src/components/docs/demos/captions-button/html/css/BasicUsage.ts
new file mode 100644
index 00000000..dbbd8dcd
--- /dev/null
+++ b/site/src/components/docs/demos/captions-button/html/css/BasicUsage.ts
@@ -0,0 +1,2 @@
+import '@videojs/html/video/player';
+import '@videojs/html/ui/captions-button';
diff --git a/site/src/components/docs/demos/captions-button/react/css/BasicUsage.css b/site/src/components/docs/demos/captions-button/react/css/BasicUsage.css
new file mode 100644
index 00000000..5b5c76f5
--- /dev/null
+++ b/site/src/components/docs/demos/captions-button/react/css/BasicUsage.css
@@ -0,0 +1,21 @@
+.react-captions-button-basic {
+ position: relative;
+}
+
+.react-captions-button-basic video {
+ width: 100%;
+}
+
+.react-captions-button-basic__button {
+ padding-block: 8px;
+ position: absolute;
+ bottom: 10px;
+ left: 10px;
+ background: rgba(255, 255, 255, 0.7);
+ backdrop-filter: blur(10px);
+ color: black;
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ border-radius: 9999px;
+ padding-inline: 20px;
+ cursor: pointer;
+}
diff --git a/site/src/components/docs/demos/captions-button/react/css/BasicUsage.tsx b/site/src/components/docs/demos/captions-button/react/css/BasicUsage.tsx
new file mode 100644
index 00000000..80e8712d
--- /dev/null
+++ b/site/src/components/docs/demos/captions-button/react/css/BasicUsage.tsx
@@ -0,0 +1,30 @@
+import { CaptionsButton, createPlayer } from '@videojs/react';
+import { Video, videoFeatures } from '@videojs/react/video';
+
+import './BasicUsage.css';
+
+const Player = createPlayer({ features: videoFeatures });
+
+export default function BasicUsage() {
+ return (
+
+
+
+ (
+
+ )}
+ />
+
+
+ );
+}
diff --git a/site/src/content/docs/reference/captions-button.mdx b/site/src/content/docs/reference/captions-button.mdx
new file mode 100644
index 00000000..a541fc91
--- /dev/null
+++ b/site/src/content/docs/reference/captions-button.mdx
@@ -0,0 +1,92 @@
+---
+title: CaptionsButton
+frameworkTitle:
+ html: media-captions-button
+description: Accessible captions toggle button with availability detection and state reflection
+---
+
+import ComponentReference from "@/components/docs/api-reference/ComponentReference.astro";
+import FrameworkCase from "@/components/docs/FrameworkCase.astro";
+import StyleCase from "@/components/docs/StyleCase.astro";
+import Demo from "@/components/docs/demos/Demo.astro";
+
+{/* React demos */}
+import BasicUsageDemoReact from "@/components/docs/demos/captions-button/react/css/BasicUsage";
+import basicUsageReactTsx from "@/components/docs/demos/captions-button/react/css/BasicUsage.tsx?raw";
+import basicUsageReactCss from "@/components/docs/demos/captions-button/react/css/BasicUsage.css?raw";
+
+{/* HTML demos */}
+import BasicUsageDemoHtml from "@/components/docs/demos/captions-button/html/css/BasicUsage.astro";
+import basicUsageHtml from "@/components/docs/demos/captions-button/html/css/BasicUsage.html?raw";
+import basicUsageHtmlCss from "@/components/docs/demos/captions-button/html/css/BasicUsage.css?raw";
+import basicUsageHtmlTs from "@/components/docs/demos/captions-button/html/css/BasicUsage.ts?raw";
+
+## Anatomy
+
+
+```tsx
+
+```
+
+
+
+```html
+
+```
+
+
+## Behavior
+
+Toggles captions and subtitles on and off. The button checks the media's text track list for tracks with `kind="captions"` or `kind="subtitles"` and reflects availability via `data-availability`.
+
+When no caption or subtitle tracks are present, `data-availability="unavailable"` is set. Use this to hide the button when there are no tracks to toggle.
+
+## Styling
+
+Style the button based on active state:
+
+```css
+media-captions-button[data-active] .icon-on { display: inline; }
+media-captions-button:not([data-active]) .icon-off { display: inline; }
+```
+
+Hide when no caption tracks are available:
+
+```css
+media-captions-button[data-availability="unavailable"] {
+ display: none;
+}
+```
+
+## Accessibility
+
+Renders a `