Skip to content

docs(site): add how-to guide for building custom components#1008

Merged
decepulis merged 11 commits intomainfrom
docs/how-to-build-your-own-component
Apr 27, 2026
Merged

docs(site): add how-to guide for building custom components#1008
decepulis merged 11 commits intomainfrom
docs/how-to-build-your-own-component

Conversation

@decepulis
Copy link
Copy Markdown
Collaborator

@decepulis decepulis commented Mar 18, 2026

Note

Depends on #1007

Add a new "Build your own component" how-to guide covering how to read player state, dispatch actions, place components in the player, style with player state, and make components accessible. Includes a full "skip intro" button example for both React and HTML frameworks.


Note

Low Risk
Low risk documentation-only change that adds a new guide and links/navigation entries; no runtime/player code is modified.

Overview
Adds a new how-to guide, how-to/build-your-own-component, explaining how to build custom player UI components for both React and HTML (state/actions access, placement in the player, and accessibility), including a full “skip intro” example.

Updates the docs navigation to surface the new guide: it’s added to the How to sidebar and linked from the concepts/overview page.

Reviewed by Cursor Bugbot for commit c5dac41. Bugbot is set up for automated code reviews on this repo. Configure here.

Copilot AI review requested due to automatic review settings March 18, 2026 15:56
@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 18, 2026

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit c5dac41
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/69efa2271db47900082b7eac
😎 Deploy Preview https://deploy-preview-1008--vjs10-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview, Comment Apr 27, 2026 5:51pm

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 18, 2026

📦 Bundle Size Report

🎨 @videojs/html — no changes
Presets (7)
Entry Size
/video (default) 28.77 kB
/video (default + hls) 161.35 kB
/video (minimal) 26.26 kB
/video (minimal + hls) 158.75 kB
/audio (default) 26.66 kB
/audio (minimal) 24.20 kB
/background 4.16 kB
Media (8)
Entry Size
/media/background-video 1.04 kB
/media/container 1.72 kB
/media/dash-video 236.54 kB
/media/hls-video 134.01 kB
/media/mux-audio 160.06 kB
/media/mux-video 160.07 kB
/media/native-hls-video 3.77 kB
/media/simple-hls-video 15.80 kB
Players (3)
Entry Size
/video/player 7.04 kB
/audio/player 5.12 kB
/background/player 3.86 kB
Skins (29)
Entry Type Size
/video/minimal-skin.css css 3.50 kB
/video/skin.css css 3.53 kB
/video/minimal-skin js 26.25 kB
/video/minimal-skin.tailwind js 26.45 kB
/video/skin js 28.77 kB
/video/skin.tailwind js 28.81 kB
/audio/minimal-skin.css css 2.54 kB
/audio/skin.css css 2.50 kB
/audio/minimal-skin js 24.20 kB
/audio/minimal-skin.tailwind js 24.38 kB
/audio/skin js 26.62 kB
/audio/skin.tailwind js 26.76 kB
/background/skin.css css 117 B
/background/skin js 1.15 kB
/live-video/minimal-skin.css css 3.50 kB
/live-video/skin.css css 3.53 kB
/live-video/minimal-skin js 25.98 kB
/live-video/minimal-skin.tailwind js 26.07 kB
/live-video/skin js 28.44 kB
/live-video/skin.tailwind js 28.54 kB
/live-audio/minimal-skin.css css 2.54 kB
/live-audio/skin.css css 2.50 kB
/live-audio/minimal-skin js 23.99 kB
/live-audio/minimal-skin.tailwind js 23.98 kB
/live-audio/skin js 26.36 kB
/live-audio/skin.tailwind js 26.42 kB
/base.css css 157 B
/shared.css css 88 B
/skin-element js 1.36 kB
UI Components (25)
Entry Size
/ui/alert-dialog 1010 B
/ui/alert-dialog-close 462 B
/ui/alert-dialog-description 390 B
/ui/alert-dialog-title 421 B
/ui/buffering-indicator 2.48 kB
/ui/captions-button 2.67 kB
/ui/cast-button 2.64 kB
/ui/compounds 4.15 kB
/ui/controls 2.35 kB
/ui/error-dialog 3.01 kB
/ui/fullscreen-button 2.62 kB
/ui/hotkey 1.85 kB
/ui/mute-button 2.67 kB
/ui/pip-button 2.63 kB
/ui/play-button 2.70 kB
/ui/playback-rate-button 2.66 kB
/ui/popover 1.84 kB
/ui/poster 2.24 kB
/ui/seek-button 2.65 kB
/ui/slider 1.52 kB
/ui/thumbnail 2.89 kB
/ui/time 2.52 kB
/ui/time-slider 3.93 kB
/ui/tooltip 2.03 kB
/ui/volume-slider 2.64 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react — no changes
Presets (7)
Entry Size
/video (default) 23.48 kB
/video (default + hls) 154.91 kB
/video (minimal) 21.11 kB
/video (minimal + hls) 152.51 kB
/audio (default) 19.06 kB
/audio (minimal) 17.61 kB
/background 755 B
Media (7)
Entry Size
/media/background-video 575 B
/media/dash-video 235.04 kB
/media/hls-video 132.49 kB
/media/mux-audio 158.70 kB
/media/mux-video 158.57 kB
/media/native-hls-video 2.26 kB
/media/simple-hls-video 14.36 kB
Skins (26)
Entry Type Size
/video/minimal-skin.css css 3.44 kB
/video/skin.css css 3.46 kB
/video/minimal-skin js 21.04 kB
/video/minimal-skin.tailwind js 24.53 kB
/video/skin js 23.40 kB
/video/skin.tailwind js 24.66 kB
/audio/minimal-skin.css css 2.44 kB
/audio/skin.css css 2.39 kB
/audio/minimal-skin js 17.52 kB
/audio/minimal-skin.tailwind js 20.04 kB
/audio/skin js 18.98 kB
/audio/skin.tailwind js 19.97 kB
/background/skin.css css 90 B
/background/skin js 272 B
/live-video/minimal-skin.css css 3.44 kB
/live-video/skin.css css 3.46 kB
/live-video/minimal-skin js 17.74 kB
/live-video/minimal-skin.tailwind js 21.15 kB
/live-video/skin js 20.14 kB
/live-video/skin.tailwind js 21.32 kB
/live-audio/minimal-skin.css css 2.44 kB
/live-audio/skin.css css 2.39 kB
/live-audio/minimal-skin js 15.72 kB
/live-audio/minimal-skin.tailwind js 18.03 kB
/live-audio/skin js 17.21 kB
/live-audio/skin.tailwind js 18.12 kB
UI Components (20)
Entry Size
/ui/alert-dialog 1.09 kB
/ui/buffering-indicator 1.79 kB
/ui/captions-button 2.02 kB
/ui/cast-button 2.04 kB
/ui/controls 1.76 kB
/ui/error-dialog 2.25 kB
/ui/fullscreen-button 2.06 kB
/ui/mute-button 2.03 kB
/ui/pip-button 2.00 kB
/ui/play-button 1.99 kB
/ui/playback-rate-button 1.89 kB
/ui/popover 1.86 kB
/ui/poster 1.67 kB
/ui/seek-button 2.10 kB
/ui/slider 2.66 kB
/ui/thumbnail 2.07 kB
/ui/time 2.06 kB
/ui/time-slider 2.36 kB
/ui/tooltip 2.17 kB
/ui/volume-slider 3.18 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core — no changes
Entries (9)
Entry Size
. 4.96 kB
/dom 11.87 kB
/dom/media/custom-media-element 1.90 kB
/dom/media/dash 234.13 kB
/dom/media/google-cast 4.07 kB
/dom/media/hls 131.85 kB
/dom/media/mux 158.10 kB
/dom/media/native-hls 1.61 kB
/dom/media/simple-hls 13.73 kB
🏷️ @videojs/element — no changes
Entries (2)
Entry Size
. 996 B
/context 943 B
📦 @videojs/store — no changes
Entries (3)
Entry Size
. 1.39 kB
/html 695 B
/react 360 B
🔧 @videojs/utils — no changes
Entries (10)
Entry Size
/array 104 B
/dom 1.92 kB
/events 319 B
/function 327 B
/object 275 B
/predicate 265 B
/string 148 B
/style 190 B
/time 478 B
/number 158 B
📦 @videojs/spf — no changes
Entries (3)
Entry Size
. 40 B
/dom 13.33 kB
/playback-engine 13.24 kB

ℹ️ How to interpret

All sizes are standalone totals (minified + brotli).

Icon Meaning
No change
🔺 Increased ≤ 10%
🔴 Increased > 10%
🔽 Decreased
🆕 New (no baseline)

Run pnpm size locally to check current sizes.

@decepulis decepulis marked this pull request as draft March 18, 2026 15:58
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new “Build your own component” how-to guide to the docs site, aimed at helping users create custom player controls that read state, dispatch actions, and remain accessible.

Changes:

  • Added a new how-to guide at how-to/build-your-own-component.mdx with React + HTML framework examples (including a “skip intro” control).
  • Added the new guide to the “How to” section of the docs sidebar.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
site/src/docs.config.ts Adds the new how-to page slug to the “How to” sidebar contents.
site/src/content/docs/how-to/build-your-own-component.mdx Introduces the full guide content and code examples for React and HTML usage patterns.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

this.addEventListener('click', this.#handleActivate);
this.addEventListener('keydown', this.#handleKeydown);
}

Comment on lines +249 to +253
const visible = time.currentTime < 30 && !playback.paused;
this.toggleAttribute('data-visible', visible);
this.style.opacity = visible ? '1' : '0';
this.style.pointerEvents = visible ? 'auto' : 'none';
}
};

#handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter' || event.key === ' ') {
Comment thread site/src/content/docs/how-to/build-your-own-component.mdx Outdated
Comment thread site/src/content/docs/how-to/build-your-own-component.mdx Outdated
Merge "Read player state" and "Dispatch actions" into a single section
that introduces features, state, and actions together. Fix HTML examples
that incorrectly mixed selectors (calling volume actions through a
playback selector). Correct Provider vs Container distinction — video-player
is the provider, not the container. Remove "Style with player state"
section that made inaccurate claims about automatic data attributes and
nonexistent design tokens. Add Actions column to features table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

<FrameworkCase frameworks={["react"]}>

For button-like controls, the <DocsLink slug="reference/use-button">`useButton`</DocsLink> hook handles keyboard activation (Enter and Space) and ARIA attributes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useButton hook is mentioned for button-like controls but not used below.

Comment on lines +175 to +182
style={{
position: 'absolute',
bottom: '5rem',
right: '1rem',
opacity: visible ? 1 : 0,
pointerEvents: visible ? 'auto' : 'none',
transition: 'opacity 200ms',
}}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep in the spirit of the library maybe we should use data attrs and CSS? E.g., data-visible

Comment on lines +209 to +210
this.addEventListener('click', this.#handleActivate);
this.addEventListener('keydown', this.#handleKeydown);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not cleaned up

Comment on lines +195 to +196
import { ReactiveElement } from '@videojs/element';
import { PlayerController, playerContext, selectTime, selectPlayback } from '@videojs/html';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should re-export ReactiveElement from html package (if we aren't yet). We also have MediaElement which extends ReactiveElement and has destroy lifecycle baked in.

Comment on lines +215 to +217
const time = this.#time.value;
const playback = this.#playback.value;
if (!time || !playback) return;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: new line above

suggestion: Comment briefly explaining why these can be undefined (store features are configured so some features might be missing)

Comment on lines +220 to +222
this.toggleAttribute('data-visible', visible);
this.style.opacity = visible ? '1' : '0';
this.style.pointerEvents = visible ? 'auto' : 'none';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a data-visible attribute, we should move the opacity and pointer-events to CSS.

@decepulis
Copy link
Copy Markdown
Collaborator Author

decepulis commented Mar 23, 2026

🤖 Style review

HTML path in "Place your component" — skeleton may not earn its place

The React path is lean: use the hook, done. The HTML path introduces ReactiveElement, PlayerController, selectors, and update() all in one skeleton code block — then the full example restates the same class with real logic. The skeleton previews without teaching.

Consider replacing the skeleton with prose that explains the pattern, then letting the full example be the first time the reader sees a complete class:

Extend ReactiveElement from @videojs/element so your component participates in the reactive update cycle. Create a PlayerController with a feature selector to subscribe to state — when that feature's state changes, your update() method is called automatically. The full example demonstrates this pattern.

This gives the HTML reader the mental model (ReactiveElement + PlayerController + selector → automatic update() calls) without a throwaway code skeleton. Shorter section, less redundancy, same information.


Written by Claude Code

Copy link
Copy Markdown
Collaborator Author

@decepulis decepulis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Technical Review

The APIs referenced are all correct — Player.usePlayer, PlayerController, playerContext, selectPlayback, selectTime, and all state/action names match the source code. Here are issues not already covered by other comments:

1. await store.play() in component render scope (line 55)

The React snippet mixes hook calls with await:

const store = Player.usePlayer();
await store.play();

The usePlayer() calls establish this as a component render function, where await is not valid. This should either be shown inside an event handler/effect, or drop the await and just note that play() returns a Promise.

2. static tagName is unused (line 199)

The full HTML example declares static tagName = 'skip-intro-button' but then registers with customElements.define('skip-intro-button', SkipIntroButtonElement). static tagName is only consumed by safeDefine() — it does nothing with direct customElements.define(). Either switch to safeDefine(SkipIntroButtonElement) or remove static tagName.

3. update() omits the changed parameter (lines 134, 213)

Both the snippet and full example override update() with no parameter:

update() {
  super.update();

The actual ReactiveElement signature is update(changed: PropertyValues) and the library's own JSDoc example passes it through: super.update(changed). The guide should match the library's convention so users learn the right pattern, especially since changed is useful for conditional update logic.

4. HTML element never sets visible text (full example)

The React example renders <button>Skip intro</button> with visible text. The HTML custom element class never sets textContent or creates a child <button>. Combined with the empty <skip-intro-button></skip-intro-button> in the placement section (line 121), the button renders visually empty — only aria-label provides text for screen readers. Should either set this.textContent = 'Skip intro' in connectedCallback(), show inner HTML in the markup, or create a shadow DOM with a <button>.

5. Unresolved TODO (line 149)

TODO: Link to concepts/accessibility once that page is merged

The concepts/accessibility page exists and is already in the sidebar, so this can be resolved now with a <DocsLink> link.

6. React full example: missing createPlayer context

The React example uses Player.usePlayer((s) => s.currentTime) and assumes it's a number. This works because createPlayer is called with features that include time — but the guide never shows the createPlayer setup. A brief note or link to createPlayer would help users understand why these properties are available and typed.


Written by Claude Code

- Use standalone `usePlayer` import from @videojs/react instead of
  `Player.usePlayer` namespace
- Switch from ReactiveElement to MediaElement from @videojs/html
- Use data-visible attr + CSS instead of inline styles (React and HTML)
- Add AbortController cleanup pattern for event listeners
- Fix ARIA button keyboard pattern (Space activates on keyup)
- Toggle tabindex when hidden to remove from tab order
- Add visible text to HTML element markup
- Resolve TODO: link to accessibility concept page
- Present full examples in tabbed code blocks (TS + CSS)
- Use Shiki focus annotations instead of inline comments
- Add explanatory comments for non-obvious patterns
- Remove unused useButton mention from accessibility section
- Add annotated skeleton in "Place your component" HTML section

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@decepulis decepulis marked this pull request as ready for review March 23, 2026 21:51
decepulis and others added 2 commits April 27, 2026 10:56
- Drop unused `static tagName` from the full skip-intro example (the
  guide registers via `customElements.define()` directly, so the field
  is dead code from a userland perspective).
- Forward `changed: PropertyValues` to `super.update()` in both the
  intro skeleton and the full example, matching first-party convention
  and the actual `ReactiveElement` signature. `PropertyValues` now
  imports from `@videojs/html` (re-exported via #1472).
- Trim the "Place your component" skeleton — drop comments and
  `customElements.define` line — so it's a minimal anchor for what's
  required, leaving the full example as the first complete class.

Addresses review feedback on #1008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop redundant "just" before "like built-in controls" (words-to-avoid).
- Replace emdash bullet pattern with colons in "You might not need a
  custom component" — five identical emdashes triggered the
  "go easy on emdashes; prefer commas, colons, or semicolons" rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- HTML "Place your component" now mentions `<media-container>` for
  fullscreen + user activity parity with React. The example moves
  `<skip-intro-button>` inside `<video-skin>` so it lives in
  `<media-container>` via the skin's default slot.
- Add `[!code focus]` markers on the wrapping elements being discussed
  in both React and HTML placement code blocks.
- Reframe the "Use player state and actions" intro to lead with
  intent ("Need access to player state or actions?") instead of
  declaration.
- Drop the prescriptive accessibility prose; the section is a heading
  + link card pointing to the dedicated guide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@decepulis
Copy link
Copy Markdown
Collaborator Author

@cursor review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit c5dac41. Configure here.

@decepulis decepulis merged commit b830bae into main Apr 27, 2026
26 checks passed
@decepulis decepulis deleted the docs/how-to-build-your-own-component branch April 27, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants