Skip to main content

Customize Quickplay Player

This guide covers UI customization for QuickplayPlayer and ContentPlayer. You can tune controls, error overlay behavior, viewport behavior, top and middle chrome content, and CSS variables without modifying bundled player code.

Choose a theme​

Use the theme prop to switch between the built-in visual themes without writing custom CSS. It's the fastest way to change the player's overall look before reaching for CSS custom properties.

<QuickplayPlayer source={source} theme="sutro" />

theme accepts 'sutro' or 'quickplay' (the default). See API reference for the full prop signature.

Customize subtitle/CC style​

While live captions are showing, viewers can open a settings panel — via a gear button next to the CC control — to customize font, size, edge style, color, and background. Style changes apply instantly to the currently showing captions, and the chosen style is remembered per device across tab refreshes, new tabs, and new sessions. No player props are required to enable this; it's built in whenever captions are available.

Try it: open the subtitle settings panel​

This demo stream ships with English captions, so you can try the full flow right here. Turn on captions with the CC button, then click the gear icon that slides in beside it to change font, size, edge style, color, or background — the currently showing captions restyle instantly.

Live Editor
function SubtitleStyleExample() {
  const source = {
    contentUrl:
      'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8',
    drmLicenseUrl: '',
    mediaType: 'hls',
    drmScheme: 'none',
  };

  return (
    <ContentPlayer
      className="w-full h-full"
      source={source}
      title="Subtitle style demo"
      subtitle="Turn on CC, then click the gear icon"
      controls
      autoPlay
      // Browsers only allow autoplay when muted. Unmute anytime from
      // the volume control.
      muted
    />
  );
}
Result
Loading...

By default, that preference is stored via a localStorage-backed provider. To persist it somewhere else instead — for example, synced to a signed-in user's account — pass your own preconfigured preferenceProvider to QuickplayPlatformProvider:

<QuickplayPlatformProvider
configUrl="https://api.example.com/config"
preferenceProvider={myAccountPreferenceProvider}
>
<App />
</QuickplayPlatformProvider>;

myAccountPreferenceProvider here is any object you've already built that implements the PreferenceProvider contract — see preferenceProvider in the API reference if you need to implement one from scratch.

The player calls your provider's get(key)/set(key, value) with one fixed key today - 'subtitleStyle' - and a value shaped like this:

{
font: string; // e.g. 'Default', 'Sans', 'Comic'
fontSize: string; // e.g. 'Default', 'Medium', 'X-Large'
fontEdge: string; // e.g. 'Default', 'Outline Dark Bold'
fontColor: string; // e.g. 'Default', 'White', 'Yellow'
fontColorOpacity: string; // e.g. 'Default', '100%', '50%'
backgroundColor: string; // e.g. 'Default', 'Black'
backgroundOpacity: string; // e.g. 'Default', '75%', '0%'
}

Each field is constrained to a fixed list of values - the same list the built-in settings panel's dropdowns offer:

FieldAllowed values
fontDefault, Mono Sans, Mono Serif, Sans, Serif, Casual, Cursive, Fantasy, Comic, Fancy, Small Caps
fontSizeDefault, X-Small, Small, Medium, Large, X-Large, XX-Large
fontEdgeDefault, Outline Dark, Outline Light, Outline Dark Bold, Outline Light Bold, Shadow Dark, Shadow Light, Shadow Dark Bold, Shadow Light Bold
fontColor / backgroundColorDefault, Black, Silver, Gray, White, Maroon, Red, Purple, Fuchsia, Green, Lime, Olive, Yellow, Navy, Blue, Teal, Aqua, Orange
fontColorOpacity / backgroundOpacityDefault, 100%, 75%, 50%, 25%, 0%

Store and return this object as-is under that key - your provider doesn't need to know the individual fields, only to round-trip whatever it's given.

Configure controls options​

Use controlsOptions to toggle optional controls and customize built-in error overlay copy and actions. Audio track selection, video quality, playback speed, and caption menus are built in and appear automatically when the stream supports them — no extra configuration is needed.

<QuickplayPlayer
source={source}
controlsOptions={{
showPipButton: true,
showCastButton: true,
showCaptionsButton: true,
showErrorOverlay: true,
errorTitle: 'Something went wrong',
errorMessage: "We couldn't play this video",
errorPrimaryLabel: 'Try again',
errorSecondaryLabel: 'Go back',
onErrorPrimary: () => window.location.reload(),
onErrorSecondary: () => window.history.back(),
}}
/>

controlsOptions supports these fields:

FieldTypeDefaultDescription
showPipButtonbooleanfalseShow the Picture-in-Picture button.
showCastButtonbooleanfalseShow the Chromecast button.
showCaptionsButtonbooleanfalseShow an inline captions toggle in addition to the captions menu.
showErrorOverlaybooleantrueShow the built-in error overlay on playback failure.
errorVariant'overlay' | 'strip''overlay'Error presentation. overlay shows a full error card. strip shows the recovery message and error code in a bottom bar. See Choose an error variant.
errorTitlestring—Heading text for the error overlay. Not shown in strip mode.
errorMessagestring—Overrides the default message for either variant.
errorPrimaryLabelstring—Primary action button label. Not shown in strip mode.
errorSecondaryLabelstring—Secondary action button label. Not shown in strip mode.
onErrorPrimary() => void—Retry callback for the overlay primary action or the strip Play button.
onErrorSecondary() => void—Callback for the secondary error action button.

Choose an error variant​

Use errorVariant to control how the player presents a playback failure. The default overlay variant shows a full error card. The strip variant is a subtler option for embedded players.

<QuickplayPlayer
source={source}
poster={posterUrl}
controlsOptions={{
errorVariant: 'strip',
onErrorPrimary: () => reauthorizeAndReplaySource(),
}}
/>

The strip variant works in both themes:

  • Shows the recovery message and error code in a bottom bar.
  • Keeps the configured poster visible.
  • Shows only Play and fullscreen controls while the error is displayed.
  • Announces the message through a polite live region.
  • Omits the title and secondary action, which appear only in overlay mode.

The default messages differ by variant. The overlay uses "Oops! Something went wrong. Please try again." The strip uses "An error has occurred. Please press the play button or reload the page to try again." Set errorMessage to override either default.

Both variants select the error code the same way. Server errors show the internal error code when an internal error exists. Other errors, and server errors without an internal error, show the hex error code. The player omits the code when it's missing. A numeric zero is displayed.

The strip Play button calls onErrorPrimary when you supply it. Otherwise it calls the player's play() method. Provide onErrorPrimary when recovery needs a new authorization or source.

Runtime errors clear after playback reaches the STARTED state, in both variants. Errors you supply from outside the player stay under your control. While an error is displayed, the controls stay visible and gestures and hotkeys can't bypass recovery. If you pass a custom errorOverlay, it takes precedence and renders once.

Use responsive controls in small players​

The player controls adapt to the width of the player, not the width of the browser window. For example, a 400px embed on a 1440px-wide page uses compact controls, and the layout updates when the player enters or exits fullscreen. This behavior applies to both the quickplay and sutro themes, and you don't need to set any props to enable it.

<div style={{ width: 400 }}>
<QuickplayPlayer source={source} />
</div>

Layouts by player width​

The player picks one of three layouts based on its measured width.

Player widthLayout
480px and narrowerCompact
481px to 767pxMedium
768px and widerWide

Try it: resize the player​

This demo keeps the page width fixed and changes only the player width. Select a width to switch between the compact, medium, and wide layouts. Select Sutro to compare both themes.

Live Editor
function ResponsiveControlsExample() {
  const widths = [320, 400, 480, 600, 767, 768, 960];
  const [width, setWidth] = useState(400);
  const [theme, setTheme] = useState('quickplay');
  const layout = width <= 480 ? 'Compact' : width < 768 ? 'Medium' : 'Wide';

  const source = {
    contentUrl:
      'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8',
    drmLicenseUrl: '',
    mediaType: 'hls',
    drmScheme: 'none',
  };

  return (
    <div>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
        {widths.map(w => (
          <button
            key={w}
            type="button"
            aria-pressed={w === width}
            onClick={() => setWidth(w)}
          >
            {w}px
          </button>
        ))}
        {['quickplay', 'sutro'].map(t => (
          <button
            key={t}
            type="button"
            aria-pressed={t === theme}
            onClick={() => setTheme(t)}
          >
            {t === 'quickplay' ? 'Quickplay' : 'Sutro'}
          </button>
        ))}
      </div>
      <p>
        Player width: {width}px ({layout} layout)
      </p>
      <div style={{ width, maxWidth: '100%', aspectRatio: '16 / 9' }}>
        <ContentPlayer
          key={theme}
          className="w-full h-full"
          source={source}
          theme={theme}
          controls
          muted
        />
      </div>
    </div>
  );
}
Result
Loading...

Compact layout behavior​

Compact players keep the controls that viewers need most and adjust the rest to fit the available space. In a compact player:

  • The 44px mute and unmute button stays available, including during ads.
  • VOD content keeps the elapsed time and seek bar. The player hides the total duration at 320px and narrower.
  • Live content keeps the seek bar and the LIVE button. The player doesn't show an elapsed or behind-live timer.
  • The settings gear collapses when captions are off, so the remaining actions stay grouped.
  • The subtitle settings panel fits inside the player. The header stays fixed, the fields scroll, and the panel includes Reset and Close actions.

Panel behavior​

Each player shows only one popover at a time. For example, opening subtitle settings closes the captions menu. The player also closes open panels when the layout changes.

Configure viewport behavior​

Use viewportOptions to control how the player reacts as it scrolls in and out of the viewport. These options let you pause offscreen playback, shrink the player into a floating picture-in-picture window, or require a click before playback starts. The IntersectionObserver tuning fields let you adjust exactly when the player is considered "in view."

viewportOptions supports these behavior flags:

FieldTypeDefaultDescription
pauseWhenOffscreenbooleanfalsePause playback automatically when the player scrolls out of view, and resume it when the player scrolls back into view. Has no effect while enableFloatingPip keeps the player visible in a floating window.
enableFloatingPipbooleanfalseShrink the player into a floating picture-in-picture window when it scrolls out of view, instead of pausing it. Takes priority over pauseWhenOffscreen when both are enabled.
requireClickToPlaybooleanfalseRequire the viewer to click the player before playback starts, even if autoplay is otherwise enabled. Overrides autoplay behavior that would otherwise trigger when the player enters the viewport.

viewportOptions also accepts these IntersectionObserver tuning fields:

FieldTypeDefaultDescription
rootElement | nullnullThe scrollable ancestor element used to calculate visibility. Defaults to the browser viewport when null.
rootMarginstring'0px'Margin around root that grows or shrinks the area considered "in view" before visibility changes trigger. Accepts CSS margin syntax, for example '100px 0px'.
thresholdnumber0The percentage of the player, expressed as a number between 0 and 1, that must be visible before it's considered in view. Higher values delay when pauseWhenOffscreen and enableFloatingPip take effect.

Replace the default top chrome​

You can supply custom top overlay content through the top-chrome slot.

<QuickplayPlayer source={source}>
<div slot="top-chrome" className="custom-header">
<img src="/logo.png" alt="Company logo" />
<div>
<h1>Custom title</h1>
<p>Custom metadata</p>
</div>
</div>
</QuickplayPlayer>

Add middle chrome content​

You can supply custom overlay content positioned over the center of the player through the middle-chrome slot. Use it for content like a call-to-action overlay or a custom replay treatment that sits above the video.

<QuickplayPlayer source={source}>
<div slot="middle-chrome" className="custom-middle">
<button onClick={handleReplay}>Watch again</button>
</div>
</QuickplayPlayer>

Apply CSS custom properties​

The player exposes CSS custom properties so you can brand controls without touching bundled CSS. Set overrides on media-controller to apply to every player on the page, or combine with className to target a specific instance.

/* All players */
media-controller {
--media-accent-color: #f97316;
--media-menu-background: #1a1a2e;
}
<QuickplayPlayer source={source} className="brand-player" />
/* One specific player */
media-controller.brand-player {
--media-accent-color: #00b4d8;
--media-primary-color: #f0f0f0;
}

Try it: set your brand's accent color​

Edit the hex values below to see the player's progress bar, seek thumb, and menu checkmarks update immediately — no CSS file or rebuild required for this preview. In your app, set the same custom properties from a CSS file or a style attribute, as shown above.

Live Editor
function BrandColorExample() {
  const source = {
    contentUrl:
      'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8',
    drmLicenseUrl: '',
    mediaType: 'hls',
    drmScheme: 'none',
  };

  // Try changing these hex values. The `.brand-color-demo` class combined
  // with `className` gives the override enough specificity to beat the
  // theme's own defaults, which are set directly on the same element.
  const accentColor = '#f97316';
  const menuBackground = '#1a1a2e';

  return (
    <>
      <style>{`
        .brand-color-demo.qp-theme--quickplay {
          --media-accent-color: ${accentColor};
          --media-menu-background: ${menuBackground};
        }
      `}</style>
      <ContentPlayer
        source={source}
        title="Brand color demo"
        className="brand-color-demo w-full h-full"
        controls
        autoPlay
        // Browsers only allow autoplay when muted. Unmute anytime from
        // the volume control.
        muted
      />
    </>
  );
}
Result
Loading...

Colors​

PropertyDefaultDescription
--media-primary-color#ffffffIcon and text fill.
--media-secondary-colortransparentControl bar background tint.
--media-accent-color#0778D6Progress bar fill, range thumb, and menu checkmark.
--media-menu-background#252732Background for overlay menus (audio, quality, etc.).
--media-tooltip-background#252732Tooltip background.
--media-tooltip-text-color#ffffffTooltip text color.
--media-live-button-indicator-color#E42121Live indicator dot.

Seek bar​

PropertyDefaultDescription
--media-range-track-backgroundrgba(255, 255, 255, 0.5)Track color.
--media-range-track-pointer-backgroundrgba(255, 255, 255, 0.5)Hover-preview pointer color.
--media-time-range-buffered-colorrgba(255, 255, 255, 0.5)Buffered portion color.

Volume slider​

PropertyDefaultDescription
--media-range-track-backgroundrgba(255, 255, 255, 0.2)Track color.
--media-range-thumb-background#ffffffThumb color.

Typography and sizing​

PropertyDefaultDescription
--media-font-familyRoboto, sans-serifFont family.
--media-text-colorsame as --media-primary-colorMain text color.
--media-control-hover-backgroundtransparentControl hover state.
--media-range-track-height4pxSeek and volume track height.
--media-range-thumb-height16pxRange thumb height.
--media-range-thumb-width16pxRange thumb width.
--media-range-thumb-border-radius50%Range thumb corner radius.
--media-tooltip-border-radius4pxTooltip corner radius.
--media-tooltip-padding5px 10pxTooltip padding.
--media-tooltip-font-size12pxTooltip font size.

Scale all controls​

--base (default 18px) scales control heights, icon sizes, and font sizes together. Use it for compact or large-format players:

media-controller {
--base: 14px; /* smaller controls */
}

Target individual controls​

For finer control, target media-chrome elements directly under media-controller.

Note: Targeting individual controls directly is not part of the stable public API and may break across minor releases. Prefer CSS custom properties where possible.

media-controller media-play-button {
width: 48px;
height: 48px;
}

media-controller media-fullscreen-button {
display: none;
}

Next steps​

Continue with feature-specific behavior and diagnostics.