media-airplay-button
Accessible AirPlay toggle button that opens the WebKit playback target picker and reflects session state
Anatomy
<AirPlayButton /><media-airplay-button></media-airplay-button>Behavior
Opens the WebKit AirPlay playback target picker and reflects session state. AirPlay is a WebKit-only feature, so the button reports availability: "unsupported" outside Safari (macOS and iOS). On supported platforms availability flips to "available" once Safari discovers at least one AirPlay receiver on the local network, and "unavailable" otherwise.
The button is hidden until availability is "available": the HTML custom element receives the native hidden attribute, and the React component returns null. An explicitly disabled, otherwise available button stays visible and focusable with aria-disabled="true" and data-disabled, but does not open the picker.
The component consumes the unified remotePlayback store feature alongside CastButton: both buttons drive their state from the same feature, but each only surfaces on its supported platform (WebKit for AirPlay, Chromium for Cast).
WebKit does not expose a "connecting" intermediate state — the state slice flips directly between "disconnected" and "connected" when the AirPlay session changes. When connected, the picker itself acts as the disconnect UI.
Styling
| Attribute | Values | Description |
|---|---|---|
data-airplay-state |
"disconnected" | "connecting" | "connected" |
Current AirPlay session state |
data-availability |
"available" | "unavailable" | "unsupported" |
Whether AirPlay is reachable on the current platform |
data-disabled |
Present / absent | Present when the button is non-interactive |
data-hidden |
Present / absent | Present on the HTML element while AirPlay is unavailable or unsupported |
Use data-airplay-state to swap icons or labels based on the session state:
/* AirPlay active */
media-airplay-button[data-airplay-state="connected"] {
color: var(--accent);
}React renders a <button> element. Add a className and use it as the selector:
/* AirPlay active */
.airplay-button[data-airplay-state="connected"] {
color: var(--accent);
}Use data-disabled to style an explicitly disabled, available button:
media-airplay-button[data-disabled] {
cursor: not-allowed;
opacity: 0.5;
}.airplay-button[data-disabled] {
cursor: not-allowed;
opacity: 0.5;
}Unavailable and unsupported buttons are hidden automatically. No availability selector or extra hiding CSS is required.
Accessibility
Renders a <button> with an automatic aria-label: “Start AirPlay” when disconnected, “Stop AirPlay” when connected. (The component also supports a "Connecting" label, but WebKit AirPlay never emits a connecting state, so that label is unreachable in practice.) Override with the label prop — either a string or a function that receives the current state. Keyboard activation: Enter / Space.
Examples
Basic Usage
import { AirPlayButton, createPlayer } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
const Player = createPlayer({ features: videoFeatures });
export default function BasicUsage() {
return (
<Player.Provider>
<Player.Container className="media-container">
<Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoPlay muted playsInline loop />
<AirPlayButton
className="media-airplay-button"
render={(props, state) => (
<button {...props}>{state.state === 'connected' ? 'Stop AirPlay' : 'Start AirPlay'}</button>
)}
/>
</Player.Container>
</Player.Provider>
);
}
.media-container {
position: relative;
}
.media-container video {
width: 100%;
}
.media-airplay-button {
position: absolute;
right: 10px;
bottom: 10px;
padding-block: 8px;
padding-inline: 20px;
color: black;
cursor: pointer;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
/* Explicitly disabled while AirPlay is available. */
.media-airplay-button[data-disabled] {
cursor: not-allowed;
opacity: 0.5;
filter: grayscale(1);
}
<video-player class="video-player">
<media-container>
<video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoplay muted
playsinline loop></video>
<media-airplay-button class="media-airplay-button">
<span class="connected">Stop AirPlay</span>
<span class="not-connected">Start AirPlay</span>
</media-airplay-button>
</media-container>
</video-player>
.video-player {
position: relative;
display: block;
}
.video-player video {
width: 100%;
}
.media-airplay-button {
position: absolute;
right: 10px;
bottom: 10px;
padding-block: 8px;
padding-inline: 20px;
color: black;
cursor: pointer;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
.media-airplay-button .connected,
.media-airplay-button .not-connected {
display: none;
}
/* Connected: AirPlay active */
.media-airplay-button[data-airplay-state="connected"] .connected {
display: inline;
}
/* Not connected: ready to AirPlay */
.media-airplay-button:not([data-airplay-state="connected"]) .not-connected {
display: inline;
}
/* Explicitly disabled while AirPlay is available. */
.media-airplay-button[data-disabled] {
cursor: not-allowed;
opacity: 0.5;
filter: grayscale(1);
}
import '@videojs/html/video/player';
import '@videojs/html/ui/airplay-button';
API Reference
Props
| Prop | Type | Default | Details |
|---|---|---|---|
disabled | boolean | false | |
| |||
label | Text | string | Text) | function | '' | |
| |||
State
render, className, and style props.| Property | Type | Details |
|---|---|---|
state | 'disconnected' | 'connecting' | 'conn... | |
| ||
availability | 'available' | 'unavailable' | 'unsupp... | |
| ||
disabled | boolean | |
| ||
label | Text | string | |
Data attributes
| Attribute | Type | Details |
|---|---|---|
data-airplay-state | 'disconnected' | 'connecting' | 'conn... | |
| ||
data-availability | 'available' | 'unavailable' | 'unsupp... | |
| ||
data-disabled | ||
| ||