diff --git a/.claude/skills/api-reference/references/builder-conventions.md b/.claude/skills/api-reference/references/builder-conventions.md
index 1729d7a2a..de90c6ace 100644
--- a/.claude/skills/api-reference/references/builder-conventions.md
+++ b/.claude/skills/api-reference/references/builder-conventions.md
@@ -8,6 +8,7 @@ Naming and file placement conventions required by the api-docs-builder at `site/
|------|------|---------|
| Core | `packages/core/src/core/ui/{name}/{name}-core.ts` | Props, State, defaultProps |
| Data attrs | `packages/core/src/core/ui/{name}/{name}-data-attrs.ts` | Data attribute definitions |
+| CSS vars | `packages/core/src/core/ui/{name}/{name}-css-vars.ts` | CSS custom property definitions (optional) |
| HTML element | `packages/html/src/ui/{name}/{name}-element.ts` | Custom element with `static tagName` |
| React parts | `packages/react/src/ui/{name}/index.parts.ts` | Multi-part detection (optional) |
@@ -21,6 +22,7 @@ The builder derives PascalCase from kebab-case using `kebabCase` from es-toolkit
| State interface | `PlayButtonState` |
| Core class | `PlayButtonCore` |
| Data attrs export | `PlayButtonDataAttrs` |
+| CSS vars export | `PlayButtonCSSVars` |
| HTML element class | `PlayButtonElement` |
| HTML tag name | `static tagName = 'media-play-button'` |
@@ -40,10 +42,16 @@ Use overrides only when the standard conversion fails (e.g., acronyms like PiP).
**Detection**: Presence of `packages/react/src/ui/{name}/index.parts.ts`.
-**Primary part identification**: The part whose HTML element file is `{name}-element.ts` (not `{name}-{part}-element.ts`). The primary part receives the shared core props/state/data-attrs.
+**Non-local re-export filtering**: Only exports with source paths starting with `./` are treated as parts. Re-exports from other directories (e.g., `../slider/index.parts`) are filtered out. This prevents domain variant components (TimeSlider, VolumeSlider) from inheriting base component parts.
+
+**Single-part fallback**: When filtering leaves only one part (typically Root), the component uses single-part mode — the remaining part's props/state/data-attrs are promoted to the top level, not nested under `parts`.
+
+**Primary part identification**: The part whose React source file instantiates the component's Core class (matches `new \w+Core\(`). The primary part receives the shared core props/state/data-attrs/css-vars.
**Non-primary parts**: Each gets its own element file at `{name}-{part}-element.ts`. Element class must be `{Name}{Part}Element` (e.g., `TimeGroupElement`).
+**Framework-divergent parts**: All parts get `platforms.react`. Parts with a matching HTML element file also get `platforms.html`. The renderer filters parts by framework — React-only parts are hidden in HTML docs.
+
**Part descriptions**: Extracted from JSDoc on the React component export:
```tsx
/** Displays a formatted time value. */
@@ -70,6 +78,7 @@ The builder fails silently for many issues — data just won't appear in the JSO
| Empty props | Interface not named `{PascalCase}Props` |
| Empty state | Interface not named `{PascalCase}State` |
| No data attributes | File missing or export not named `{PascalCase}DataAttrs` |
+| No CSS vars | File missing or export not named `{PascalCase}CSSVars` |
| No HTML tag | Element file missing or no `static tagName` |
| No part descriptions | Missing JSDoc on React component exports |
| Wrong PascalCase | Need a `NAME_OVERRIDES` entry |
diff --git a/.claude/skills/api-reference/references/mdx-structure.md b/.claude/skills/api-reference/references/mdx-structure.md
index 74456f0dc..da74f2d65 100644
--- a/.claude/skills/api-reference/references/mdx-structure.md
+++ b/.claude/skills/api-reference/references/mdx-structure.md
@@ -195,7 +195,7 @@ Always the last element in the file:
```
-The component auto-renders Props, State, Data Attributes for single-part and all Parts for multi-part.
+The component auto-renders Props, State, Data Attributes, and CSS Custom Properties for single-part and all Parts for multi-part. For multi-part components, React-only parts are hidden in HTML docs via framework filtering.
### Required Astro Component Imports
diff --git a/internal/design/site/api-docs-builder.md b/internal/design/site/api-docs-builder.md
index b348dbe3f..01593e41c 100644
--- a/internal/design/site/api-docs-builder.md
+++ b/internal/design/site/api-docs-builder.md
@@ -6,8 +6,8 @@ rendered documentation tables. When implementation diverges from this spec, this
Inspired by [Base UI](https://github.com/mui/base-ui)'s API reference system. Base UI generates one JSON
file per component part, and each part gets its own props and data-attributes tables. Our system
aspires to the same philosophy but has a known architectural limitation: a single Props/State
-interface per component at the core level means only one part (the "primary") can own props and
-state. Non-primary parts are documented with a tag name and description only.
+interface per component at the core level means only one part (the "primary") can own core-level props
+and state. Non-primary parts get shared data attributes, custom React-specific props, and a description.
**Principles:**
@@ -208,6 +208,7 @@ kebab-name `{name}`:
|------|----------|----------|
| Core | `packages/core/src/core/ui/{name}/{name}-core.ts` | Yes |
| Data attrs | `packages/core/src/core/ui/{name}/{name}-data-attrs.ts` | No |
+| CSS vars | `packages/core/src/core/ui/{name}/{name}-css-vars.ts` | No |
| HTML element | `packages/html/src/ui/{name}/{name}-element.ts` | No |
| React parts index | `packages/react/src/ui/{name}/index.parts.ts` | No |
@@ -217,6 +218,11 @@ PascalCase name.
**Multi-part detection:** A component is multi-part if and only if `index.parts.ts` exists.
+**Domain variant components:** Components like TimeSlider and VolumeSlider that share base
+logic (e.g., from `slider/`) must still have their own directories under `core/ui/`. The
+builder discovers components by directory — files nested inside a shared directory (like
+`slider/time-slider-core.ts`) won't be found.
+
### 2b. Component extraction
#### Single-part components
@@ -227,6 +233,7 @@ Extract from the three source files and merge into one reference object.
|--------|----------|
| Core file | Props interface members (if present), State interface members (if present), `defaultProps` values (if present) |
| Data attrs file | Data attribute names, JSDoc descriptions, and inferred types (if file exists) |
+| CSS vars file | CSS custom property names and JSDoc descriptions (if file exists) |
| HTML element file | `static tagName` value (if file exists) |
**Naming conventions the builder depends on:**
@@ -237,6 +244,7 @@ Extract from the three source files and merge into one reference object.
| State interface | `{PascalCase}State` |
| Core class | `{PascalCase}Core` |
| Data attrs export | `{PascalCase}DataAttrs` |
+| CSS vars export | `{PascalCase}CSSVars` |
| HTML element class | `{PascalCase}Element` |
**All symbols are optional.** Only the Core class is required. If a component has no Props
@@ -258,6 +266,7 @@ component is skipped with a warning.
| Both Props and State | Warn, skip component | Component omitted |
| `defaultProps` static | Silent | Props have no `default` field |
| Data-attrs file/export | Silent | `dataAttributes: {}` |
+| CSS-vars file/export | Silent | `cssCustomProperties: {}` |
| JSDoc on a data attribute | Silent | `description: ""` (empty string) |
| HTML element file | Silent | No `platforms.html` section |
@@ -298,40 +307,71 @@ state type, the builder falls back to the existing `@type` JSDoc tag extraction.
The builder discovers parts from `index.parts.ts` and matches them to HTML element files.
+**Re-exported parts:** When `index.parts.ts` re-exports parts from another component (source
+path doesn't start with `./`), the builder resolves the re-export back to its origin. It
+parses the origin's `index.parts.ts`, matches each re-exported name to the original local
+export, then derives the kebab segment and HTML element file from the **origin component** —
+not the current one. Re-exported parts are never primary. For example, TimeSlider re-exports
+Buffer, Fill, Thumb, Track, and Value from `../slider/index.parts`; each resolves to the
+Slider component's HTML element files (`slider-buffer-element.ts`, etc.).
+
+**Single-part fallback:** When all exports are local and filtering leaves only one part, the
+component uses single-part mode. The remaining part (typically Root) becomes the top-level
+component — its props/state/data-attrs/CSS-vars are promoted to the component level, not
+nested under `parts`. Components with re-exported parts (like TimeSlider and VolumeSlider)
+always produce multi-part output since the re-exports are resolved rather than filtered.
+
**Primary vs. sub-part convention:**
-Every multi-part component has one **primary part** and one or more **sub-parts**. The
-convention is file naming:
+Every multi-part component has one **primary part** and one or more **sub-parts**.
-- The **root element** file is `{component}-element.ts` (e.g., `time-element.ts`)
-- **Sub-part element** files are `{component}-{part}-element.ts` (e.g., `time-group-element.ts`)
-- The primary part is whichever part maps to the root element — i.e., the part that does NOT
- have a `{component}-{part}-element.ts` file, because its element IS the root element.
+**Primary part:** The part whose React source file instantiates the component's Core class
+(matches `new \w+Core\(`). This captures the architectural relationship — the primary part
+owns the Core — and is immune to import ordering and framework-divergent element structures.
-This is a naming convention, not configuration. The root element file always exists for the
-primary part; sub-parts always have their own element files.
+Sub-part element files use the naming convention `{component}-{part}-element.ts` (e.g.,
+`time-group-element.ts`) for HTML tag resolution.
**Part-to-element matching:**
-For each named export in `index.parts.ts`:
+For each local named export in `index.parts.ts`:
1. Derive kebab segment from the export's source path (e.g., `./time-value` → `value`)
2. Look for `{component}-{part}-element.ts` in the HTML directory
3. If found → sub-part (gets its own tag name)
-4. If not found → primary part (gets the root element's tag name from `{component}-element.ts`)
+4. If not found AND `{component}-element.ts` exists → check via Core-instantiation for primary
+
+For re-exported parts: use the origin component's kebab and HTML directory for element file
+lookup. The element class name is derived from the filename convention (`kebabToPascal` of the
+basename, e.g., `slider-buffer-element.ts` → `SliderBufferElement`), not from the current
+component's PascalCase name. This same convention-based derivation is also used for local
+non-primary parts.
-**What the primary part gets:** The shared core Props, State, data attributes, and the root
-element's tag name.
+**What the primary part gets:** The shared core Props, State, data attributes, CSS custom
+properties, and the root element's tag name.
-**What sub-parts get:** Their own tag name, a description (from React component JSDoc), and
-empty props/state/dataAttributes.
+**What sub-parts get:** Their own tag name, a description (from React JSDoc), shared data
+attributes from the component's `*-data-attrs.ts` file (when the sub-part's React source
+references `stateAttrMap`), and custom React-specific props (own members on the
+`{LocalName}Props` interface, excluding inherited `UIComponentProps` members and `children`).
+State and CSS custom properties remain empty.
-**What the top-level component gets:** Empty props, state, dataAttributes, and empty platforms.
-All meaningful data lives in the `parts` record.
+For re-exported sub-parts, data attributes come from the **origin** component's data-attrs file
+(e.g., TimeSlider.Fill uses Slider's data-attrs, not TimeSlider's), because the builder can't
+resolve spread entries and the origin file has the complete set that sub-parts inherit.
+
+**What the top-level component gets:** Empty props, state, dataAttributes, cssCustomProperties,
+and empty platforms. All meaningful data lives in the `parts` record.
+
+**Framework-divergent parts:** Parts discovered from `index.parts.ts` always get
+`platforms.react`. Parts with a matching HTML element file also get `platforms.html`. The
+renderer filters parts by framework — only parts with the current framework's platform
+entry are shown. This handles cases like Popover where Arrow, Popup, and Trigger are
+React-only compound parts with no HTML element counterparts.
> **Known limitation:** Our architecture has a single Props/State interface per component at the
-> core level, so only the primary part can own them. In Base UI, each part has its own props
-> independently. If a sub-part needs its own props in the future, the core architecture would
-> need per-part interfaces.
+> core level, so only the primary part can own core-level props and state. Sub-parts can declare
+> custom React-specific props (e.g., `SliderValueProps.type`), which the builder extracts from
+> the React source. In Base UI, each part has its own props independently.
### 2c. Util discovery
@@ -470,9 +510,11 @@ ComponentReference
├── props: Record — Empty {} for multi-part top-level
├── state: Record — Empty {} for multi-part top-level
├── dataAttributes: Record
+├── cssCustomProperties: Record
├── platforms
-│ └── html?
-│ └── tagName: string — e.g., "media-toggle-button"
+│ ├── html?
+│ │ └── tagName: string — e.g., "media-toggle-button"
+│ └── react? — Present for React-discovered parts (object, no fields)
└── parts?: Record — Only for multi-part components
└── [partId]
├── name: string — PascalCase part name (e.g., "Track")
@@ -480,9 +522,17 @@ ComponentReference
├── props: Record
├── state: Record
├── dataAttributes: Record
+ ├── cssCustomProperties: Record
└── platforms
- └── html?
- └── tagName: string
+ ├── html?
+ │ └── tagName: string
+ └── react? — Always present (parts come from index.parts.ts)
+```
+
+**CSSVarDef:**
+
+```
+└── description: string — JSDoc description of the CSS custom property
```
**PropDef:**
@@ -596,6 +646,7 @@ omitted (absence means not required). This keeps JSON files small.
"description": "Present when the button is disabled."
}
},
+ "cssCustomProperties": {},
"platforms": {
"html": {
"tagName": "media-toggle-button"
@@ -612,6 +663,7 @@ omitted (absence means not required). This keeps JSON files small.
"props": {},
"state": {},
"dataAttributes": {},
+ "cssCustomProperties": {},
"platforms": {},
"parts": {
"indicator": {
@@ -651,10 +703,12 @@ omitted (absence means not required). This keeps JSON files small.
"type": "'empty' | 'partial' | 'full'"
}
},
+ "cssCustomProperties": {},
"platforms": {
"html": {
"tagName": "media-meter"
- }
+ },
+ "react": {}
}
},
"track": {
@@ -663,10 +717,12 @@ omitted (absence means not required). This keeps JSON files small.
"props": {},
"state": {},
"dataAttributes": {},
+ "cssCustomProperties": {},
"platforms": {
"html": {
"tagName": "media-meter-track"
- }
+ },
+ "react": {}
}
},
"fill": {
@@ -675,10 +731,12 @@ omitted (absence means not required). This keeps JSON files small.
"props": {},
"state": {},
"dataAttributes": {},
+ "cssCustomProperties": {},
"platforms": {
"html": {
"tagName": "media-meter-fill"
- }
+ },
+ "react": {}
}
}
}
@@ -826,7 +884,8 @@ Both consume the same model, which prevents anchor drift (TOC links matching ren
H2 "API Reference" id="api-reference"
├─ H3 "Props" id="props" (if props non-empty)
├─ H3 "State" id="state" (if state non-empty)
- └─ H3 "Data attributes" id="data-attributes" (if dataAttributes non-empty)
+ ├─ H3 "Data attributes" id="data-attributes" (if dataAttributes non-empty)
+ └─ H3 "CSS custom properties" id="css-custom-properties" (if cssCustomProperties non-empty)
```
**Multi-part** heading structure:
@@ -837,7 +896,8 @@ H2 "API Reference" id="api-reference"
│ or "{part.tagName}" (HTML)
│ ├─ H4 "Props" id="{partId}-props" (if props non-empty)
│ ├─ H4 "State" id="{partId}-state" (if state non-empty)
- │ └─ H4 "Data attributes" id="{partId}-data-attributes" (if dataAttributes non-empty)
+ │ ├─ H4 "Data attributes" id="{partId}-data-attributes" (if dataAttributes non-empty)
+ │ └─ H4 "CSS custom properties" id="{partId}-css-custom-properties" (if cssCustomProperties non-empty)
├─ H3 next part...
└─ ...
```
@@ -846,6 +906,10 @@ Multi-part H3 headings are framework-aware: React sees the PascalCase part name
HTML sees the tag name (e.g., "media-meter-track"). The TOC emits both variants with
`frameworks` metadata so the correct one displays per framework.
+**Framework filtering:** The TOC and rendered output only emit headings for parts the current
+framework supports (derived from `platforms` keys). React-only parts (those with
+`platforms.react` but no `platforms.html`) are hidden when viewing HTML docs.
+
### 5b. Util reference model
**Single-overload** heading structure:
@@ -1051,7 +1115,25 @@ renders inline:
> `ReturnType` — Description text here.
-### 6f. Multi-part component rendering
+### 6f. CSS custom properties table (components)
+
+Rendered by `ApiCSSVarsTable` → `CSSVarRow` → `DetailRow`. Uses the same disclosure
+pattern as data attributes tables but with no type column.
+
+**Columns:**
+
+| Variable | |
+|----------|-|
+
+- **Variable** — CSS custom property name in monospace (e.g., `--media-slider-fill`).
+- **(toggle)** — Disclosure triangle. Present if the row has a description.
+
+**Disclosure panel** (when expanded):
+
+Contains a description list (`
`):
+- **Description** — Markdown-rendered description. Only shown if `description` is present.
+
+### 6g. Multi-part component rendering
For multi-part components, the top-level has no tables (all empty). Each part renders as:
@@ -1061,6 +1143,7 @@ H3: Part name (framework-specific label)
H4: Props (if non-empty) → Props table
H4: State (if non-empty) → State table
H4: Data attributes (if non-empty) → Data attributes table
+ H4: CSS custom properties (if non-empty) → CSS custom properties table
```
**State section preamble** (framework-specific):
@@ -1068,7 +1151,7 @@ H3: Part name (framework-specific label)
- **React:** "State is accessible via the `render`, `className`, and `style` props."
- **HTML:** "State is reflected as data attributes for CSS styling."
-### 6g. Multi-overload util rendering
+### 6h. Multi-overload util rendering
Each overload renders as:
@@ -1085,7 +1168,7 @@ heading text. Otherwise fall back to "Overload {N}".
**Heading ID:** Labeled overloads use the kebab-case slug of the label (e.g., `"Video"` →
`id="video"`). Unlabeled overloads use `id="overload-{n}"`.
-### 6h. Disclosure panel interaction
+### 6i. Disclosure panel interaction
The `DetailRow` component implements an expandable disclosure pattern:
diff --git a/packages/core/src/core/index.ts b/packages/core/src/core/index.ts
index 0585d11e0..d2ab5c72f 100644
--- a/packages/core/src/core/index.ts
+++ b/packages/core/src/core/index.ts
@@ -24,14 +24,17 @@ export * from './ui/seek-button/seek-button-data-attrs';
export * from './ui/slider/slider-core';
export * from './ui/slider/slider-css-vars';
export * from './ui/slider/slider-data-attrs';
-export * from './ui/slider/time-slider-core';
-export * from './ui/slider/time-slider-data-attrs';
-export * from './ui/slider/volume-slider-core';
export * from './ui/thumbnail/thumbnail-core';
export * from './ui/thumbnail/thumbnail-data-attrs';
export * from './ui/thumbnail/thumbnail-media-fragment';
export * from './ui/thumbnail/types';
export * from './ui/time/time-core';
export * from './ui/time/time-data-attrs';
+export * from './ui/time-slider/time-slider-core';
+export * from './ui/time-slider/time-slider-css-vars';
+export * from './ui/time-slider/time-slider-data-attrs';
export * from './ui/transition';
export * from './ui/types';
+export * from './ui/volume-slider/volume-slider-core';
+export * from './ui/volume-slider/volume-slider-css-vars';
+export * from './ui/volume-slider/volume-slider-data-attrs';
diff --git a/packages/core/src/core/ui/slider/tests/time-slider-core.test.ts b/packages/core/src/core/ui/time-slider/tests/time-slider-core.test.ts
similarity index 99%
rename from packages/core/src/core/ui/slider/tests/time-slider-core.test.ts
rename to packages/core/src/core/ui/time-slider/tests/time-slider-core.test.ts
index f0f4a3216..af22ea714 100644
--- a/packages/core/src/core/ui/slider/tests/time-slider-core.test.ts
+++ b/packages/core/src/core/ui/time-slider/tests/time-slider-core.test.ts
@@ -1,7 +1,7 @@
import { describe, expect, it, vi } from 'vitest';
import type { MediaBufferState, MediaTimeState } from '../../../media/state';
-import type { SliderInteraction } from '../slider-core';
+import type { SliderInteraction } from '../../slider/slider-core';
import { TimeSliderCore } from '../time-slider-core';
type TimeSliderMedia = MediaTimeState & MediaBufferState;
diff --git a/packages/core/src/core/ui/slider/time-slider-core.ts b/packages/core/src/core/ui/time-slider/time-slider-core.ts
similarity index 98%
rename from packages/core/src/core/ui/slider/time-slider-core.ts
rename to packages/core/src/core/ui/time-slider/time-slider-core.ts
index 1197fb3f2..303a8d841 100644
--- a/packages/core/src/core/ui/slider/time-slider-core.ts
+++ b/packages/core/src/core/ui/time-slider/time-slider-core.ts
@@ -4,7 +4,7 @@ import { formatTimeAsPhrase } from '@videojs/utils/time';
import type { NonNullableObject } from '@videojs/utils/types';
import type { MediaBufferState, MediaTimeState } from '../../media/state';
-import { type SliderBaseProps, SliderCore, type SliderInteraction, type SliderState } from './slider-core';
+import { type SliderBaseProps, SliderCore, type SliderInteraction, type SliderState } from '../slider/slider-core';
export interface TimeSliderProps extends SliderBaseProps {
/** Trailing-edge throttle (ms) for seek requests during drag. */
diff --git a/packages/core/src/core/ui/time-slider/time-slider-css-vars.ts b/packages/core/src/core/ui/time-slider/time-slider-css-vars.ts
new file mode 100644
index 000000000..28e7b8d2e
--- /dev/null
+++ b/packages/core/src/core/ui/time-slider/time-slider-css-vars.ts
@@ -0,0 +1,11 @@
+// Values from SliderCSSVars — duplicated because the api-docs-builder extracts
+// JSDoc from the object literal, so we need component-specific descriptions here.
+/** CSS custom property names for time slider visual state. */
+export const TimeSliderCSSVars = {
+ /** Fill level percentage (0–100), representing current playback position. */
+ fill: '--media-slider-fill',
+ /** Pointer position percentage (0–100), tracking the cursor along the slider. */
+ pointer: '--media-slider-pointer',
+ /** Buffer level percentage (0–100), indicating how much media has been buffered. */
+ buffer: '--media-slider-buffer',
+} as const;
diff --git a/packages/core/src/core/ui/slider/time-slider-data-attrs.ts b/packages/core/src/core/ui/time-slider/time-slider-data-attrs.ts
similarity index 82%
rename from packages/core/src/core/ui/slider/time-slider-data-attrs.ts
rename to packages/core/src/core/ui/time-slider/time-slider-data-attrs.ts
index 06f817ec8..7391544d9 100644
--- a/packages/core/src/core/ui/slider/time-slider-data-attrs.ts
+++ b/packages/core/src/core/ui/time-slider/time-slider-data-attrs.ts
@@ -1,5 +1,5 @@
+import { SliderDataAttrs } from '../slider/slider-data-attrs';
import type { StateAttrMap } from '../types';
-import { SliderDataAttrs } from './slider-data-attrs';
import type { TimeSliderState } from './time-slider-core';
export const TimeSliderDataAttrs = {
diff --git a/packages/core/src/core/ui/slider/tests/volume-slider-core.test.ts b/packages/core/src/core/ui/volume-slider/tests/volume-slider-core.test.ts
similarity index 98%
rename from packages/core/src/core/ui/slider/tests/volume-slider-core.test.ts
rename to packages/core/src/core/ui/volume-slider/tests/volume-slider-core.test.ts
index 3239cce63..3ccbd9693 100644
--- a/packages/core/src/core/ui/slider/tests/volume-slider-core.test.ts
+++ b/packages/core/src/core/ui/volume-slider/tests/volume-slider-core.test.ts
@@ -1,7 +1,7 @@
import { describe, expect, it, vi } from 'vitest';
import type { MediaVolumeState } from '../../../media/state';
-import type { SliderInteraction } from '../slider-core';
+import type { SliderInteraction } from '../../slider/slider-core';
import { VolumeSliderCore } from '../volume-slider-core';
function createInteraction(overrides: Partial = {}): SliderInteraction {
diff --git a/packages/core/src/core/ui/slider/volume-slider-core.ts b/packages/core/src/core/ui/volume-slider/volume-slider-core.ts
similarity index 97%
rename from packages/core/src/core/ui/slider/volume-slider-core.ts
rename to packages/core/src/core/ui/volume-slider/volume-slider-core.ts
index d9fc6f6a9..61eacb632 100644
--- a/packages/core/src/core/ui/slider/volume-slider-core.ts
+++ b/packages/core/src/core/ui/volume-slider/volume-slider-core.ts
@@ -2,7 +2,7 @@ import { defaults } from '@videojs/utils/object';
import type { NonNullableObject } from '@videojs/utils/types';
import type { MediaVolumeState } from '../../media/state';
-import { type SliderBaseProps, SliderCore, type SliderInteraction, type SliderState } from './slider-core';
+import { type SliderBaseProps, SliderCore, type SliderInteraction, type SliderState } from '../slider/slider-core';
export interface VolumeSliderProps extends SliderBaseProps {}
diff --git a/packages/core/src/core/ui/volume-slider/volume-slider-css-vars.ts b/packages/core/src/core/ui/volume-slider/volume-slider-css-vars.ts
new file mode 100644
index 000000000..e70537152
--- /dev/null
+++ b/packages/core/src/core/ui/volume-slider/volume-slider-css-vars.ts
@@ -0,0 +1,9 @@
+// Values from SliderCSSVars — duplicated because the api-docs-builder extracts
+// JSDoc from the object literal, so we need component-specific descriptions here.
+/** CSS custom property names for volume slider visual state. */
+export const VolumeSliderCSSVars = {
+ /** Fill level percentage (0–100), representing the current volume level. */
+ fill: '--media-slider-fill',
+ /** Pointer position percentage (0–100), tracking the cursor along the slider. */
+ pointer: '--media-slider-pointer',
+} as const;
diff --git a/packages/core/src/core/ui/volume-slider/volume-slider-data-attrs.ts b/packages/core/src/core/ui/volume-slider/volume-slider-data-attrs.ts
new file mode 100644
index 000000000..99aea06de
--- /dev/null
+++ b/packages/core/src/core/ui/volume-slider/volume-slider-data-attrs.ts
@@ -0,0 +1,7 @@
+import { SliderDataAttrs } from '../slider/slider-data-attrs';
+import type { StateAttrMap } from '../types';
+import type { VolumeSliderState } from './volume-slider-core';
+
+export const VolumeSliderDataAttrs = {
+ ...SliderDataAttrs,
+} as const satisfies StateAttrMap;
diff --git a/packages/core/src/dom/tests/test-helpers.ts b/packages/core/src/dom/tests/test-helpers.ts
index f5c497a86..84d849560 100644
--- a/packages/core/src/dom/tests/test-helpers.ts
+++ b/packages/core/src/dom/tests/test-helpers.ts
@@ -1,5 +1,5 @@
import type { SliderState } from '../../core/ui/slider/slider-core';
-import type { TimeSliderState } from '../../core/ui/slider/time-slider-core';
+import type { TimeSliderState } from '../../core/ui/time-slider/time-slider-core';
// ---------------------------------------------------------------------------
// Mock Video
diff --git a/packages/core/src/dom/ui/slider-css-vars.ts b/packages/core/src/dom/ui/slider-css-vars.ts
index d483e5de0..3a22526c5 100644
--- a/packages/core/src/dom/ui/slider-css-vars.ts
+++ b/packages/core/src/dom/ui/slider-css-vars.ts
@@ -1,6 +1,6 @@
import type { SliderState } from '../../core/ui/slider/slider-core';
import { SliderCSSVars } from '../../core/ui/slider/slider-css-vars';
-import type { TimeSliderState } from '../../core/ui/slider/time-slider-core';
+import type { TimeSliderState } from '../../core/ui/time-slider/time-slider-core';
export function getSliderCSSVars(state: SliderState): Record {
return {
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 6d753048f..de9059426 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -39,7 +39,7 @@ export { MuteButton, type MuteButtonProps } from './ui/mute-button/mute-button';
export { PiPButton, type PiPButtonProps } from './ui/pip-button/pip-button';
export { PlayButton, type PlayButtonProps } from './ui/play-button/play-button';
export { PlaybackRateButton, type PlaybackRateButtonProps } from './ui/playback-rate-button/playback-rate-button';
-export { Popover } from './ui/popover';
+export { Popover, type PopoverContextValue, usePopoverContext } from './ui/popover';
export { Poster, type PosterProps } from './ui/poster/poster';
export { SeekButton, type SeekButtonProps } from './ui/seek-button/seek-button';
export { Slider } from './ui/slider';
diff --git a/packages/react/src/ui/popover/index.parts.ts b/packages/react/src/ui/popover/index.parts.ts
index 139fa7e1d..9d7ac7fbf 100644
--- a/packages/react/src/ui/popover/index.parts.ts
+++ b/packages/react/src/ui/popover/index.parts.ts
@@ -1,5 +1,4 @@
export { PopoverArrow as Arrow, type PopoverArrowProps as ArrowProps } from './popover-arrow';
-export { type PopoverContextValue, usePopoverContext } from './popover-context';
export { PopoverPopup as Popup, type PopoverPopupProps as PopupProps } from './popover-popup';
export { PopoverRoot as Root, type PopoverRootProps as RootProps } from './popover-root';
export { PopoverTrigger as Trigger, type PopoverTriggerProps as TriggerProps } from './popover-trigger';
diff --git a/packages/react/src/ui/popover/index.ts b/packages/react/src/ui/popover/index.ts
index 9d2d8d70b..f4f698df9 100644
--- a/packages/react/src/ui/popover/index.ts
+++ b/packages/react/src/ui/popover/index.ts
@@ -1 +1,2 @@
export * as Popover from './index.parts';
+export { type PopoverContextValue, usePopoverContext } from './popover-context';
diff --git a/packages/react/src/ui/popover/popover-arrow.tsx b/packages/react/src/ui/popover/popover-arrow.tsx
index 411fb0545..efb145e6f 100644
--- a/packages/react/src/ui/popover/popover-arrow.tsx
+++ b/packages/react/src/ui/popover/popover-arrow.tsx
@@ -9,6 +9,7 @@ import { usePopoverContext } from './popover-context';
export interface PopoverArrowProps extends UIComponentProps<'div', PopoverState> {}
+/** Decorative arrow pointing from the popup toward the trigger. Hidden from assistive technology. */
export const PopoverArrow = forwardRef(function PopoverArrow(
{ render, className, style, ...elementProps },
forwardedRef
diff --git a/packages/react/src/ui/popover/popover-popup.tsx b/packages/react/src/ui/popover/popover-popup.tsx
index b23d9fe6b..dd2b85bfa 100644
--- a/packages/react/src/ui/popover/popover-popup.tsx
+++ b/packages/react/src/ui/popover/popover-popup.tsx
@@ -15,6 +15,7 @@ export interface PopoverPopupProps extends UIComponentProps<'div', PopoverState>
const POPOVER_RESET: CSSProperties = { position: 'fixed', inset: 'auto', margin: 0 };
+/** Container for the popover content. Positioned relative to the trigger using CSS anchor positioning with a JavaScript fallback. */
export const PopoverPopup = forwardRef(function PopoverPopup(
{ render, className, style, ...elementProps },
forwardedRef
diff --git a/packages/react/src/ui/popover/popover-trigger.tsx b/packages/react/src/ui/popover/popover-trigger.tsx
index d8a1924f6..315070cb4 100644
--- a/packages/react/src/ui/popover/popover-trigger.tsx
+++ b/packages/react/src/ui/popover/popover-trigger.tsx
@@ -10,6 +10,7 @@ import { usePopoverContext } from './popover-context';
export interface PopoverTriggerProps extends UIComponentProps<'button', PopoverState> {}
+/** Button that toggles the popover visibility. Renders a `