agentleFS
mikemai2awesomemikemai2awesome/agent-skills/skills/brightcove-player/SKILL.md

brightcove-player

Skill17 starsChanged 6 months agoMITView on GitHub

Style and fully customize the Brightcove video player UI — control bar, play button, progress bar, volume, captions, playlists, responsive sizing, and skins. Use this skill whenever the user mentions Brightcove, video-js player styling, customizing a Brightcove player, changing player colors/layout/controls, embedding a Brightcove player, making it responsive, player skins or themes, Brightcove Studio styling, or working with Brightcove playlists or captions. Also use it when the user is working with `.video-js`, `vjs-*` CSS classes, or `bc()` / `videojs.getPlayer()` / `videojs()` JavaScript APIs.

The file

We split this skill into the rules it gives an agent

6 rules. About 0 are common, 0 are distinctive, and 6 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    When a user says "player id myPlayer", they almost always mean the element id, not a Studio player config named myPlayer.

    line 48
  2. Very specific:

    Never leave placeholder text like ACCOUNT_ID or PLAYER_ID in a demo file — it will produce a blank page with console errors.

    line 57
  3. Very specific:

    Use OKLCH — perceptually uniform, so adjusting lightness for variants is predictable.

    line 160
  4. Very specific:

    Use the color-scheme property and the light-dark() CSS function to switch token values based on system preference:

    line 166
  5. Very specific:

    Use the wrapper element pattern (see above) — it already covers aspect-ratio, width: 100%, and the container declaration needed for …

    line 465
  6. Very specific:

    Use the {PLAYER_CLASS} selector token only inside Studio-hosted stylesheets (not in page <style> tags) — it resolves to the player's …

    line 506

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Brightcove Player Customization

Brightcove players are built on Video.js. Every visual element is targetable via .vjs-* CSS classes. The tricky parts are specificity (the player ships with its own stylesheet) and the iframe vs. in-page embed split (iframe players block inline CSS entirely).

Always use physical CSS properties (width, height, max-width, top, left, etc.) — never logical properties (inline-size, block-size, inset-inline-start, etc.). Video.js itself uses physical properties throughout, and mixing logical properties into overrides creates inconsistency and can cause specificity surprises.

Player script URL and ID terminology

Brightcove uses three distinct IDs that are easy to confuse:

TermWhat it isExample
Account IDNumeric Brightcove account identifier1752604059001
Player config IDThe player configuration created in Studiodefault (or a UUID)
HTML element idThe id attribute on <video-js> — used by videojs.getPlayer()myPlayer

The CDN script URL is built from the account ID and player config ID — not the HTML element id:

https://players.brightcove.net/{account_id}/{player_config_id}_default/index.min.js

So if the account is 1752604059001 and the Studio player config is named default, the script URL is:

<script src="https://players.brightcove.net/1752604059001/default_default/index.min.js"></script>

The <video-js> element's id attribute (myPlayer, heroPlayer, etc.) is separate — it's just a DOM handle for videojs.getPlayer():

<video-js
  id="myPlayer" <!-- HTML element id — what getPlayer() uses -->
  data-account="1752604059001" <!-- Brightcove account ID — used in the script URL -->
  data-player="default" <!-- Brightcove player config ID — used in the script URL -->
  data-embed="default"
  data-video-id="4825279519001"
  class="video-js"
  controls
></video-js>

When a user says "player id myPlayer", they almost always mean the element id, not a Studio player config named myPlayer. If no Brightcove player config ID is specified, default to data-player="default" and the default_default script URL.

Demo / preview pages — when producing a self-contained demo HTML file, always use these known-good values so the player actually loads:

  • Account: 1752604059001
  • Player config: default
  • Video: 4825279519001
  • Script: https://players.brightcove.net/1752604059001/default_default/index.min.js

Never leave placeholder text like ACCOUNT_ID or PLAYER_ID in a demo file — it will produce a blank page with console errors.


Embed type — decide first

Embed typeWhere CSS livesJS access
Advanced (in-page)<style> tag on the page OR Studio stylesheetFull
Standard (iframe)Studio stylesheet only — page <style> won't workLimited

For iframe players, upload a CSS file to a public URL and add it in Studio → Players → Plugins → Stylesheets, then republish.

For in-page embeds, a <style> block on the same page is the fastest approach.


Beating Brightcove's stylesheet

The player's own stylesheet is loaded late and carries high specificity. The recommended approach is to use both techniques together:

Unnamed cascade layer — CSS layers declared with a name come before unnamed layers. Putting your overrides in an unnamed @layer block makes them beat everything, including Brightcove's injected stylesheet.

!important — even inside an unnamed layer, add !important on every property. Brightcove occasionally injects inline styles at runtime, and only !important beats those.

/* Named layers declared first — unnamed layer implicitly ranks above all of them */
@layer config, resets, components;

/* Unnamed layer + !important: covers both the stylesheet and runtime inline styles */
@layer {
  .c-player .video-js .vjs-big-play-button {
    background-color: var(--videojs-play-btn-bg) !important;
  }
}

Wrapper element pattern

Wrap <video-js> in a container element rather than styling it from the page root. This gives you:

  • An easy way to control player width and aspect ratio
  • A container query root scoped to the player width (not viewport width)
  • A clean specificity bump via class nesting
<div class="c-player">
  <video-js
    id="myPlayer"
    data-video-id="..."
    data-account="..."
    data-player="default"
    data-embed="default"
    class="video-js"
    skin="false"
    controls
  ></video-js>
</div>

Always add skin="false" on the <video-js> element when doing a custom skin. It disables Brightcove's default skin stylesheet, giving you a clean baseline with far fewer specificity fights.

.c-player {
  width: 100%;
  max-width: 56rem;
  aspect-ratio: 16 / 9;

  .video-js {
    width: 100% !important;
    height: 100% !important;
    /* Declare a container for container queries scoped to player width */
    container: video / inline-size !important;
  }
}

Design tokens

Define all tokens on :root (or .c-player if scoping tightly). Use --videojs-* prefix to keep player tokens distinct from page tokens.

:root {
  --videojs-fg: oklch(10% 0.01 250);
  --videojs-fg-subtle: oklch(45% 0 0 / 0.85);
  --videojs-bg-accent: oklch(49% 0.14 250);
  --videojs-bg-accent-hover: color-mix(
    in oklch,
    var(--videojs-bg-accent),
    black 10%
  );
  --videojs-bg-control: oklch(100% 0 0 / 0.97);
  --videojs-bg-progress-holder: oklch(0% 0 0 / 0.14);
  --videojs-bg-progress-play: oklch(100% 0 0 / 0.12);
  --videojs-border: oklch(80% 0 0 / 0.6);
  --videojs-border-subtle: oklch(80% 0 0 / 0.4);
}

Use OKLCH — perceptually uniform, so adjusting lightness for variants is predictable. Use color-mix(in oklch, var(--base-color), black 10%) rather than hard-coding separate values.


Light / dark theming

Use the color-scheme property and the light-dark() CSS function to switch token values based on system preference:

:root {
  color-scheme: light dark; /* enables system preference detection */

  --videojs-bg-accent: light-dark(oklch(49% 0.14 250), oklch(56% 0.16 250));
  --videojs-bg-control: light-dark(
    oklch(100% 0 0 / 0.97),
    oklch(22% 0.064 259 / 0.97)
  );
}

/* Override for explicit theme choice */
:root[data-theme="light"] {
  color-scheme: light;
}
:root[data-theme="dark"] {
  color-scheme: dark;
}

Always initialize data-theme from prefers-color-scheme on page load so the explicit toggle starts in sync with the system preference — otherwise users on dark OS get a light flash before JS runs:

function setTheme(value) {
  document.documentElement.dataset.theme = value;
  /* update any toggle buttons with aria-pressed here */
}

/* Read system preference and set immediately */
setTheme(
  window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
);

Play button

SelectorTargets
.video-js .vjs-big-play-buttonButton container (size, shape, background, border, position)
.video-js .vjs-big-play-button .vjs-icon-placeholderPlay icon inside the button
.video-js .vjs-big-play-button .vjs-icon-placeholder:beforeIcon glyph (font-size, color)
.video-js:hover .vjs-big-play-buttonButton on player hover
.video-js .vjs-big-play-button:hoverButton on direct hover
.video-js .vjs-big-play-button:focusButton on focus
.video-js.vjs-mouse .vjs-big-play-buttonButton during mouse interaction
#myPlayerID .vjs-big-play-buttonPlayer-specific override (highest specificity)

Centering reliably: use top: 50%; left: 50%; transform: translate(-50%, -50%) and margin: 0 — the default margin-based offset from Video.js doesn't account for custom button sizes.

Fluid sizing with clamp() and vi units — use viewport-inline units so the button scales with the player width rather than staying fixed. Always do this instead of a static rem value:

:root {
  --videojs-big-btn: clamp(2rem, 8vi, 4.5rem);
  --videojs-big-btn-icon-size: clamp(1rem, 6vi, 2rem);
}

.video-js .vjs-big-play-button {
  width: var(--videojs-big-btn) !important;
  height: var(--videojs-big-btn) !important;
  font-size: var(--videojs-big-btn-icon-size) !important;
  line-height: var(--videojs-big-btn) !important;
}

Change accessible label text:

videojs.getPlayer("myPlayer").ready(function () {
  this.getChild("bigPlayButton").controlText("Watch video");
});

Control bar

SelectorTargets
.video-js .vjs-control-barBar container (background, height, padding)
.video-js .vjs-control-bar *All descendants — useful for resetting text-shadow globally
.video-js .vjs-control-bar .vjs-controlIndividual control items (color, spacing)
.video-js .vjs-control-bar .vjs-buttonButton elements
.video-js .vjs-control-bar .vjs-button:hoverButton hover state
.video-js:not(.vjs-has-started) .vjs-control-barBar before playback has started (opacity, pointer-events)
.video-js.vjs-quality-menu .vjs-quality-menu-button-HD-flag::afterHD quality badge

Video.js ships with a text-shadow on control icons — reset it explicitly via .vjs-control-bar, .vjs-control-bar *, .vjs-menu * if your design doesn't use it.

backdrop-filter requires a vendor prefix for Safari — always pair them:

.video-js .vjs-control-bar {
  -webkit-backdrop-filter: blur(12px) !important;
  backdrop-filter: blur(12px) !important;
}

Via JS:

videojs.getPlayer("myPlayer").ready(function () {
  this.controlBar.hide(); /* or .show() */
});

Progress / seek bar

SelectorTargets
.video-js .vjs-progress-controlOuter wrapper — set align-items: flex-end to anchor the bar to the bottom edge as its hit area grows on hover
.video-js .vjs-progress-holderTrack container (height, transition)
.video-js .vjs-progress-control:hover .vjs-progress-holderExpanded track height on hover
.video-js .vjs-play-progressPlayed portion (color)
.video-js .vjs-play-progress::beforePlay head dot (font-size to shrink, top to re-center)
.video-js .vjs-load-progressBuffered portion
.video-js .vjs-load-progress divBuffered sub-segments
.video-js .vjs-slider-barUnplayed track background
.video-js .vjs-progress-holder.vjs-sliderSlider track background

Volume

SelectorTargets
.video-js .vjs-volume-panelPanel wrapper — controls expand/collapse width and transition timing
.video-js .vjs-volume-panel:hoverExpanded state on hover
.video-js .vjs-volume-panel.vjs-hoverExpanded via keyboard/focus
.video-js .vjs-volume-panel.vjs-slider-activeActive while scrubbing
.video-js .vjs-volume-controlInner slider control (width, visibility transition)
.video-js .vjs-volume-control.vjs-volume-control-horizontalHorizontal layout alignment
.video-js .vjs-volume-levelFilled volume bar (color)
.video-js .vjs-volume-level::beforeVolume thumb dot (color)
.video-js .vjs-volume-bar.vjs-slider-bar.vjs-slider.vjs-slider-horizontalUnfilled track (background)

For the horizontal inline panel, use asymmetric transition delays — open fast (no delay), close with a delay so the cursor can escape without the panel collapsing mid-move.

Switch to vertical volume via JS options:

bc("myPlayer", {
  controlBar: {
    volumePanel: { inline: false, vertical: true },
  },
});

Time display & tooltip

SelectorTargets
.video-js .vjs-time-controlBase wrapper (hidden by default; use container query to show at wider widths)
.video-js .vjs-current-timeCurrent time value
.video-js .vjs-durationTotal duration value
.video-js .vjs-time-dividerSeparator between current time and duration
.video-js .vjs-remaining-timeTime remaining (hide when current + duration are both shown)
.video-js .vjs-time-tooltipSeek position tooltip that appears on progress bar hover

Container queries require container: video / inline-size on .video-js (set in the wrapper section above).


Duration badge (pre-play overlay)

A custom element injected into the player to show total duration before playback, then hidden on play. Scope it with @container video (inline-size >= 24rem) so it only appears when the player is wide enough. Use a has-played class on .video-js to hide it on play — cleaner than DOM removal and reusable for any pre-play overlays.

For the full CSS and JS implementation, see references/snippets.md.


Dock text (title / description overlay)

The dock text appears at the top of the player. Use .vjs-dock-text (not .vjs-title-bar) — they coexist but serve different purposes.

SelectorTargets
.video-js .vjs-dock-textOverlay container (background gradient, padding)
.video-js .vjs-dock-titleTitle text
.video-js .vjs-dock-descriptionDescription text
.video-js .vjs-title-barAlternative title bar overlay at the top
.video-js .vjs-title-bar-titleTitle in the title bar
.video-js .vjs-title-bar-descriptionDescription in the title bar

Popup menus

SelectorTargets
.video-js .vjs-menu-button-popup .vjs-menuMenu wrapper
.video-js .vjs-menu-button-popup .vjs-menu .vjs-menu-contentMenu content box (background, border, font)
.video-js .vjs-menu li.vjs-menu-itemIndividual menu items
.video-js .vjs-menu li.vjs-menu-item:hoverItem hover state
.video-js .vjs-menu li.vjs-menu-item:focusItem focus state
.video-js .vjs-menu li.vjs-selectedCurrently selected/active item
.video-js .vjs-menu li.vjs-selected:hoverSelected item hover (needs its own rule to avoid being overridden)

Context menu

The right-click context menu is a separate menu from the popup menus above — it uses .vjs-contextmenu-ui-menu. Always theme it alongside the popup menus or it will look unstyled against your custom skin.

SelectorTargets
.vjs-contextmenu-ui-menu .vjs-menu-contentMenu box (background, color)
@layer {
  .c-player .vjs-contextmenu-ui-menu .vjs-menu-content {
    color: var(--videojs-text) !important;
    background-color: var(--videojs-menu-bg) !important;
  }
}

Captions / subtitles

Adding captions

The CC button only appears when the player has at least one text track. When using data-video-id, the player loads its source asynchronously — a <track> element inside <video-js> is often ignored. Use addRemoteTextTrack() on the loadeddata event instead.

Always use the ensureTrack pattern — it handles source changes (multiple loadeddata firings) and deduplicates tracks so they aren't added twice. The pattern uses a captionsReady guard and calls addRemoteTextTrack() on loadeddata. For the full implementation (including multi-language support), see references/captions.md.

Local dev caveat: browsers block .vtt files loaded via file://. Serve from a local HTTP server (e.g. npx serve .).

Styling caption text

For caption text selectors, supported/unsupported properties, and Safari caveats, see references/captions.md.


Caption settings dialog

The dialog uses .vjs-text-track-settings — not .vjs-caption-settings. For the full selector table, see references/captions.md.


Adding a custom element to the control bar

videojs.getPlayer("myPlayer").ready(function () {
  var btn = document.createElement("div");
  btn.className = "vjs-control vjs-button my-custom-btn";
  btn.innerHTML = '<span class="vjs-icon-placeholder">★</span>';

  var spacer = document.querySelector(".vjs-spacer");
  spacer.style.justifyContent = "flex-end";
  spacer.appendChild(btn);
});

Fullscreen button

SelectorTargets
.video-js .vjs-fullscreen-controlButton (color, display)
.video-js .vjs-fullscreen-control:hoverButton hover state
.video-js .vjs-fullscreen-control .vjs-icon-placeholder:beforeButton icon glyph

To remove it on iOS (native fullscreen is handled by the browser), check videojs.browser.IS_IOS inside .ready() and remove the .vjs-fullscreen-control element from the DOM.


Playlist

SelectorTargets
.vjs-mouse.vjs-playlistPlaylist container (background, text color)
.vjs-playlist-itemIndividual item
.vjs-playlist-item:hoverItem hover state
.vjs-playlist-item.vjs-selectedCurrently playing item
.vjs-playlist-verticalVertical layout (flex column)
.vjs-playlist-horizontalHorizontal layout (flex row, overflow)
.vjs-playlist .vjs-playlist-thumbnailThumbnail image
.vjs-playlist .vjs-playlist-title-containerTitle area
.vjs-playlist .vjs-playlist-durationDuration badge on each item

Responsive sizing

aspect-ratio has universal browser support. The old padding-top intrinsic ratio trick is no longer necessary.

Option A — wrapper + aspect-ratio (recommended)

Use the wrapper element pattern (see above) — it already covers aspect-ratio, width: 100%, and the container declaration needed for container queries.

Option B — built-in Video.js fluid classes

Add to <video-js>:

ClassRatio
vjs-fluid2.4:1 (default)
vjs-16-916:9
vjs-4-34:3

Or configure in Studio: Players → Player Information → Sizing → Responsive.


Fixed sizing

<video-js width="960" height="540" ...></video-js>

Or CSS:

.video-js {
  width: 960px !important;
  height: 540px !important;
}

Studio workflow checklist

  1. Players module → select or create a player
  2. Styling tab → set colors, skin, and basic appearance via GUI
  3. Plugins → Stylesheets → add a URL to a hosted CSS file for anything Studio's GUI can't do
  4. Plugins → Scripts → add a URL to a hosted JS file for runtime customization
  5. Publish & Embed → Publish Changes — required after any Studio edit

Use the {PLAYER_CLASS} selector token only inside Studio-hosted stylesheets (not in page <style> tags) — it resolves to the player's generated class at publish time.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.