Skip to content

refactor(packages)!: replace button availability with disabled and hidden state - #1474

Merged
mihar-22 merged 22 commits into
mainfrom
refactor/button-disabled-hidden
Jul 31, 2026
Merged

refactor(packages)!: replace button availability with disabled and hidden state#1474
mihar-22 merged 22 commits into
mainfrom
refactor/button-disabled-hidden

Conversation

@mihar-22

@mihar-22 mihar-22 commented Apr 28, 2026

Copy link
Copy Markdown
Member

Replace the available boolean on buttons with disabled and hidden states that follow standard ARIA patterns — aria-disabled for non-interactive controls and HTML hidden for unsupported features.


Note

High Risk
Wide breaking API and UX change across core, HTML, React, and skins (including Cast state rename and when controls appear), affecting accessibility, toolbars, and integrators who styled data-availability.

Overview
Breaking change: toolbar media buttons now expose derived disabled and hidden state (plus data-disabled / data-hidden) instead of relying on skins or consumer CSS to hide controls via data-availability.

Non-interactive controls use aria-disabled from core state (including when a feature isn’t ready), while useless controls are removed with the native hidden attribute on HTML custom elements and null in React via a new isSupported hook on createMediaButton. Raw data-availability is still exposed for styling and debugging.

Per-control rules differ: PiP, fullscreen, AirPlay, and captions hide until available; Cast hides only when unsupported but stays visible and aria-disabled when supported with no device. CastButton state renames castStateconnection. toggle paths recompute disabled state before acting; Cast/fullscreen/PiP can propagate media API errors while HTML/React UI layers catch async rejections in dev.

Skins drop availability-based display: none and [disabled] styling in favor of aria-disabled. Docs, demos, e2e (e.g. PiP on WebKit), and an internal disabled/hidden design note are updated to match.

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

@netlify

netlify Bot commented Apr 28, 2026

Copy link
Copy Markdown

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit fa204b0
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/6a6bee7e45ec160009d8f13e
😎 Deploy Preview https://deploy-preview-1474--vjs10-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

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

@vercel

vercel Bot commented Apr 28, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview Jul 31, 2026 12:39am

Request Review

@github-actions

github-actions Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

🎨 @videojs/html — no changes
Presets (7)
Entry Initial Lazy
/video (default) 53.73 kB 51.74 kB
/video (default + hls) 189.59 kB 51.74 kB
/video (minimal) 53.57 kB 51.74 kB
/video (minimal + hls) 189.57 kB 51.74 kB
/audio (default) 45.94 kB 51.74 kB
/audio (minimal) 42.45 kB 51.74 kB
/background 5.00 kB 51.74 kB
Media (12)
Entry Initial Lazy
/media/background-video 1.14 kB
/media/container 2.58 kB 51.74 kB
/media/dash-video 211.14 kB
/media/google-cast 6.46 kB
/media/hlsjs-video 137.92 kB
/media/mux-audio 138.88 kB
/media/mux-data 153.85 kB
/media/mux-video 139.06 kB
/media/native-hls-video 5.34 kB
/media/simple-hls-audio-only 19.08 kB
/media/simple-hls-video 23.50 kB
/media/vimeo-video 12.31 kB
Players (5)
Entry Initial Lazy
/video/player 9.01 kB 51.74 kB
/audio/player 6.20 kB 51.74 kB
/background/player 4.73 kB 51.74 kB
/live-video/player 8.42 kB 51.74 kB
/live-audio/player 6.22 kB 51.74 kB
Skins (30)
Entry Type Initial Lazy
/video/minimal-skin.css css 5.92 kB
/video/skin.css css 5.93 kB
/video/minimal-skin js 53.55 kB 51.74 kB
/video/minimal-skin.tailwind js 54.21 kB 51.74 kB
/video/skin js 53.72 kB 51.74 kB
/video/skin.tailwind js 54.50 kB 51.74 kB
/audio/minimal-skin.css css 4.12 kB
/audio/skin.css css 4.06 kB
/audio/minimal-skin js 42.41 kB 51.74 kB
/audio/minimal-skin.tailwind js 42.95 kB 51.74 kB
/audio/skin js 45.96 kB 51.74 kB
/audio/skin.tailwind js 46.41 kB 51.74 kB
/background/skin.css css 133 B
/background/skin js 1.14 kB
/live-video/minimal-skin.css css 5.92 kB
/live-video/skin.css css 5.93 kB
/live-video/minimal-skin js 49.84 kB 51.74 kB
/live-video/minimal-skin.tailwind js 50.49 kB 51.74 kB
/live-video/skin js 52.22 kB 51.74 kB
/live-video/skin.tailwind js 52.83 kB 51.74 kB
/live-audio/minimal-skin.css css 4.12 kB
/live-audio/skin.css css 4.06 kB
/live-audio/minimal-skin js 34.59 kB 51.74 kB
/live-audio/minimal-skin.tailwind js 34.05 kB 51.74 kB
/live-audio/skin js 38.22 kB 51.74 kB
/live-audio/skin.tailwind js 37.84 kB 51.74 kB
/global.css css 183 B
/shared.css css 153 B
/tailwind.css css 161 B
/skin-element js 1.51 kB
UI Components (39)
Entry Initial
/ui/airplay-button 2.56 kB
/ui/alert-dialog 2.93 kB
/ui/alert-dialog-close 2.51 kB
/ui/alert-dialog-description 2.46 kB
/ui/alert-dialog-title 2.43 kB
/ui/audio-track-radio-group 3.03 kB
/ui/buffering-indicator 2.72 kB
/ui/captions-button 2.57 kB
/ui/captions-radio-group 3.04 kB
/ui/cast-button 2.54 kB
/ui/compounds 3.31 kB
/ui/controls 3.05 kB
/ui/error-dialog 3.07 kB
/ui/fullscreen-button 2.57 kB
/ui/hotkey 2.61 kB
/ui/menu 3.09 kB
/ui/mute-button 2.60 kB
/ui/pip-button 2.49 kB
/ui/play-button 2.60 kB
/ui/playback-rate-button 2.66 kB
/ui/playback-rate-radio-group 2.86 kB
/ui/popover 3.19 kB
/ui/poster 2.43 kB
/ui/quality-radio-group 3.05 kB
/ui/seek-button 2.56 kB
/ui/seek-indicator 2.78 kB
/ui/seek-indicator-value 381 B
/ui/slider 3.08 kB
/ui/status-announcer 2.49 kB
/ui/status-indicator 2.58 kB
/ui/status-indicator-value 438 B
/ui/thumbnail 2.66 kB
/ui/time 3.06 kB
/ui/time-slider 3.09 kB
/ui/tooltip 2.91 kB
/ui/volume-indicator 2.53 kB
/ui/volume-indicator-fill 366 B
/ui/volume-indicator-value 367 B
/ui/volume-slider 3.08 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react — no changes
Presets (7)
Entry Initial Lazy
/video (default) 42.34 kB 51.74 kB
/video (default + hls) 176.98 kB 51.74 kB
/video (minimal) 42.61 kB 51.74 kB
/video (minimal + hls) 177.26 kB 51.74 kB
/audio (default) 34.57 kB 51.74 kB
/audio (minimal) 34.65 kB 51.74 kB
/background 581 B
Media (11)
Entry Initial
/media/background-video 394 B
/media/dash-video 209.54 kB
/media/google-cast 5.35 kB
/media/hlsjs-video 136.40 kB
/media/mux-audio 137.23 kB
/media/mux-data 152.83 kB
/media/mux-video 137.38 kB
/media/native-hls-video 3.54 kB
/media/simple-hls-audio-only 17.36 kB
/media/simple-hls-video 21.81 kB
/media/vimeo-video 10.46 kB
Skins (27)
Entry Type Initial Lazy
/tailwind.css css 161 B
/video/minimal-skin.css css 5.79 kB
/video/skin.css css 5.78 kB
/video/minimal-skin js 42.61 kB 51.74 kB
/video/minimal-skin.tailwind js 48.78 kB 51.74 kB
/video/skin js 42.32 kB 51.74 kB
/video/skin.tailwind js 48.49 kB 51.74 kB
/audio/minimal-skin.css css 3.94 kB
/audio/skin.css css 3.87 kB
/audio/minimal-skin js 34.59 kB 51.74 kB
/audio/minimal-skin.tailwind js 37.08 kB 51.74 kB
/audio/skin js 34.53 kB 51.74 kB
/audio/skin.tailwind js 38.92 kB 51.74 kB
/background/skin.css css 90 B
/background/skin js 272 B
/live-video/minimal-skin.css css 5.79 kB
/live-video/skin.css css 5.78 kB
/live-video/minimal-skin js 37.52 kB 51.74 kB
/live-video/minimal-skin.tailwind js 43.69 kB 51.74 kB
/live-video/skin js 37.53 kB 51.74 kB
/live-video/skin.tailwind js 43.85 kB 51.74 kB
/live-audio/minimal-skin.css css 3.94 kB
/live-audio/skin.css css 3.87 kB
/live-audio/minimal-skin js 25.06 kB 51.74 kB
/live-audio/minimal-skin.tailwind js 28.35 kB 51.74 kB
/live-audio/skin js 25.11 kB 51.74 kB
/live-audio/skin.tailwind js 28.48 kB 51.74 kB
UI Components (33)
Entry Initial
/ui/airplay-button 2.59 kB
/ui/alert-dialog 2.81 kB
/ui/audio-track 2.50 kB
/ui/buffering-indicator 2.67 kB
/ui/captions-button 2.63 kB
/ui/captions-radio-group 2.49 kB
/ui/cast-button 2.58 kB
/ui/controls 2.64 kB
/ui/error-dialog 2.58 kB
/ui/fullscreen-button 2.64 kB
/ui/gesture 2.52 kB
/ui/hotkey 2.64 kB
/ui/live-button 2.53 kB
/ui/menu 2.87 kB
/ui/mute-button 2.59 kB
/ui/pip-button 2.63 kB
/ui/play-button 2.59 kB
/ui/playback-rate 2.58 kB
/ui/playback-rate-button 2.61 kB
/ui/popover 3.29 kB
/ui/poster 2.63 kB
/ui/quality 2.47 kB
/ui/seek-button 2.52 kB
/ui/seek-indicator 2.79 kB
/ui/slider 2.60 kB
/ui/status-announcer 2.44 kB
/ui/status-indicator 2.51 kB
/ui/thumbnail 2.65 kB
/ui/time 2.38 kB
/ui/time-slider 2.62 kB
/ui/tooltip 3.21 kB
/ui/volume-indicator 2.44 kB
/ui/volume-slider 2.70 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core — no changes
Entries (73)
Entry Initial Lazy
. 11.38 kB
/dom 18.27 kB
/i18n 3.03 kB 51.74 kB
/i18n/locales/all 31.40 kB
/i18n/locales/ar 1.14 kB
/i18n/locales/az 1.04 kB
/i18n/locales/bg 1.21 kB
/i18n/locales/bn 1.23 kB
/i18n/locales/bs 951 B
/i18n/locales/ca 1.00 kB
/i18n/locales/cs 1022 B
/i18n/locales/cy 977 B
/i18n/locales/da 943 B
/i18n/locales/de 1.02 kB
/i18n/locales/el 1.40 kB
/i18n/locales/en 732 B
/i18n/locales/es 959 B
/i18n/locales/et 1019 B
/i18n/locales/eu 983 B
/i18n/locales/fa 1.14 kB
/i18n/locales/fi 997 B
/i18n/locales/fr 1.01 kB
/i18n/locales/gd 1.03 kB
/i18n/locales/gl 960 B
/i18n/locales/he 1.06 kB
/i18n/locales/hi 1.24 kB
/i18n/locales/hr 986 B
/i18n/locales/hu 1.02 kB
/i18n/locales/it 997 B
/i18n/locales/ja 1.13 kB
/i18n/locales/ko 1.08 kB
/i18n/locales/lv 1.02 kB
/i18n/locales/mr 1.24 kB
/i18n/locales/nb 928 B
/i18n/locales/ne 1.23 kB
/i18n/locales/nl 967 B
/i18n/locales/nn 931 B
/i18n/locales/oc 1.01 kB
/i18n/locales/pl 1.06 kB
/i18n/locales/pt 988 B
/i18n/locales/pt-BR 988 B
/i18n/locales/pt-PT 953 B
/i18n/locales/ro 1.01 kB
/i18n/locales/ru 1.29 kB
/i18n/locales/sk 1.03 kB
/i18n/locales/sl 987 B
/i18n/locales/sr 1018 B
/i18n/locales/sv 962 B
/i18n/locales/te 1.26 kB
/i18n/locales/th 1.22 kB
/i18n/locales/tr 1.01 kB
/i18n/locales/uk 1.32 kB
/i18n/locales/vi 1.03 kB
/i18n/locales/zh 977 B
/i18n/locales/zh-CN 977 B
/i18n/locales/zh-TW 972 B
/i18n/text/airplay 101 B
/i18n/text/buttons 137 B
/i18n/text/captions 93 B
/i18n/text/cast 114 B
/i18n/text/common 90 B
/i18n/text/container 75 B
/i18n/text/errors 277 B
/i18n/text/fullscreen 98 B
/i18n/text/live 126 B
/i18n/text/menu 249 B
/i18n/text/pip 101 B
/i18n/text/playback 80 B
/i18n/text/seek 105 B
/i18n/text/slider 65 B
/i18n/text/status 222 B
/i18n/text/time 214 B
/i18n/text/volume 133 B
🏷️ @videojs/element — no changes
Entries (2)
Entry Initial
. 996 B
/context 943 B
📦 @videojs/store — no changes
Entries (3)
Entry Initial
. 1.39 kB
/html 696 B
/react 361 B
🔧 @videojs/utils — no changes
Entries (12)
Entry Initial
/array 104 B
/dom 3.16 kB
/events 319 B
/function 327 B
/jwt 176 B
/object 505 B
/predicate 265 B
/percent 281 B
/string 239 B
/style 190 B
/time 813 B
/number 158 B
📦 @videojs/media — no changes
Entries (14)
Entry Initial
. 959 B
/dom/audio-host 1.11 kB
/dom/custom-media-element 2.09 kB
/dom/dash 208.99 kB
/dom/google-cast 4.06 kB
/dom/hls-js 136.00 kB
/dom/media-host 1.21 kB
/dom/media-played-ranges 576 B
/dom/mux 159.41 kB
/dom/native-hls 3.02 kB
/dom/simple-hls 21.23 kB
/dom/simple-hls-audio-only 16.86 kB
/dom/video-host 1.40 kB
/dom/vimeo 9.88 kB
📦 @videojs/spf — no changes
Entries (5)
Entry Initial
. 4.46 kB
/dom 6.50 kB
/hls 17.86 kB
/media-tracks 504 B
/background-video 13.30 kB

ℹ️ How to interpret

JS sizes are initial static graph totals (minified + brotli). Lazy dynamic chunks are shown separately when present.

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

Run pnpm size locally to check current initial sizes.

@mihar-22
mihar-22 marked this pull request as ready for review April 28, 2026 06:19
@mihar-22
mihar-22 requested a review from luwes April 28, 2026 06:20
Comment thread packages/skins/src/default/css/components/button.css
Comment thread packages/core/src/core/ui/cast-button/cast-button-core.ts Outdated
Comment thread packages/html/src/ui/media-button-element.ts Outdated
Comment thread packages/skins/src/default/css/components/button.css
Comment thread packages/skins/src/default/css/components/button.css Outdated
Comment thread packages/react/src/ui/create-media-button.tsx
Comment thread packages/html/src/ui/media-button-element.ts
Comment thread apps/e2e/tests/video-controls.spec.ts Outdated
Comment thread apps/e2e/tests/video-controls.spec.ts Outdated
Comment thread packages/skins/src/default/tailwind/components/button.ts
Comment thread packages/react/src/ui/create-media-button.tsx Outdated
mihar-22 and others added 19 commits July 30, 2026 17:25
…dden state

Cast, fullscreen, and pip buttons now expose `disabled` (non-interactive)
and `hidden` (unsupported) state derived from `availability` and the
`disabled` prop, instead of relying on the raw availability enum at the
attribute layer.

- `getAttrs` returns `aria-disabled` from state and the native HTML
  `hidden` attribute when the feature is unsupported. New `data-disabled`
  and `data-hidden` data attribute mappings ride along.
- `toggle` short-circuits on `state.disabled` and otherwise awaits the
  underlying media call directly, propagating errors to the caller
  instead of swallowing them.
- `MediaButtonElement` and `createMediaButton` now wrap the activation
  in try/catch with a `__DEV__` console.error and rethrow so callers
  see the original failure.
- React buttons pass `isSupported: (s) => !s.hidden` so unsupported
  features render `null` rather than a hidden `<button>`.

Aligns with the WAI-ARIA APG toolbar pattern (focusable disabled
controls) documented in `internal/design/ui/disabled-hidden.md`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace `data-[availability=...]:hidden` with `data-[disabled]` styling
classes to match the new disabled/hidden button state model. Hidden
buttons use the native HTML hidden attribute; disabled buttons get
reduced opacity and grayscale via data-disabled.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PiP is unsupported on WebKit so the button receives the `hidden`
attribute and is removed from the layout. Only assert
`data-availability` when the pip button is visible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tons

Update the fullscreen and pip button reference pages plus the features
concept page to describe the new `disabled`/`hidden` state model:
HTML `hidden` for unsupported environments (or `null` in React) and
`data-disabled` for non-interactive styling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document the two pre-existing fields alongside the newly added
disabled/hidden so the component reference table renders complete
descriptions for every cast button state property.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The skin's `display: flex` (and `grid` on the icon variant) outranks the
user-agent `[hidden] { display: none }` rule on specificity, so feature
buttons stayed visible when the cast/fullscreen/pip cores set the native
`hidden` attribute.

Add a `&[hidden] { display: none }` rule under the high-specificity
skin selector in both default and minimal CSS, and a `[&[hidden]]:hidden`
class in the Tailwind variants so the same override works for the
Tailwind-compiled skins.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…omponent override

Move the native `hidden` attribute override from the button component into
the skin reset so any authored template that sets `[hidden]` stays hidden,
not just media buttons. Use the doubled `[hidden][hidden]` selector under
the skin root to outrank component-level `display: flex/grid` declarations.
The Tailwind root composition gets the equivalent `[&_[hidden][hidden]]:hidden`
class.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The captions button previously relied on the now-removed
`data-availability="unavailable"` skin rule to hide itself when no
caption tracks were present. Extend the same `disabled`/`hidden` model
already applied to cast/fullscreen/pip buttons so it stays hidden in
that state without depending on availability-specific CSS.

- Add `disabled` and `hidden` to `CaptionsButtonState`, derived from
  the `disabled` prop and whether any caption/subtitle tracks exist.
- `getAttrs` returns `aria-disabled` from state and the native HTML
  `hidden` attribute when no tracks are available.
- `toggle` short-circuits on `state.disabled`.
- `data-disabled` and `data-hidden` data attribute mappings ride along.
- React captions button passes `isSupported: (s) => !s.hidden` so it
  renders `null` when no tracks are present.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously unavailable buttons used `display: none`, which masked the
`:active { scale: 0.98 }` rule. Now that `[data-disabled]` keeps the
button visible but non-interactive, guard the press animation with
`:not([disabled]):not([data-disabled])` (and the Tailwind equivalent
`not-disabled:not-data-disabled:active:*`) so disabled buttons no
longer give misleading press feedback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

async toggle(state: MediaRemotePlaybackState): Promise<void> {
if (this.#props.disabled) return;
async toggle(media: MediaRemotePlaybackState): Promise<void> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

question: is it state or media - the type looks like a state? Ideally they should match?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'll have to follow-up and look into how state was modelled for AirPlay.

Comment on lines +24 to +27
/** Non-interactive but still focusable (mirrors `aria-disabled`). */
disabled: boolean;
/** Whether the button is hidden until AirPlay is available. */
hidden: boolean;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit(non-blocking): we seem to repeat this a lot, is it worth having it in a shared/common button type perhaps? Same for the data attributes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Right now for the state and mainly data attrs we're intentionally repetitive to simplify the API extraction. It keeps it simple and easy to review/read per component. Leaving it for now but happy to revisit.

Comment thread site/src/content/docs/reference/cast-button.mdx
@mihar-22
mihar-22 merged commit 066227d into main Jul 31, 2026
27 checks passed
@mihar-22
mihar-22 deleted the refactor/button-disabled-hidden branch July 31, 2026 18:37
@luwes luwes mentioned this pull request Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants