Skip to main content
Light Dark System

Video Embed

<cw-video-embed> | CwVideoEmbed
Since 1.6 experimental

A lazy-loading facade for embedding third-party video (YouTube, Vimeo, etc.) without paying its cost until someone actually wants to watch.

Most third-party video embeds (YouTube, Vimeo, and similar) load a meaningful amount of JavaScript and start tracking scripts the moment their <iframe> is added to the page — even if nobody ever presses play. <cw-video-embed> avoids that: it shows a static thumbnail and a play button, and only activates the real embed once someone actually clicks.

Supply the real embed markup — typically an <iframe> — inside an inert <template> element. Its content is never rendered until activation, so nothing inside it loads early.

<cw-video-embed thumbnail="https://images.unsplash.com/photo-1611162617213-7d7a39e9b1d7?w=800&h=450&fit=crop" label="Play: Example video">
  <template>
    <iframe
      src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
      title="Example video"
      allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
      allowfullscreen
    ></iframe>
  </template>
</cw-video-embed>
import CwVideoEmbed from '@cordwainer/cw-elements/dist/react/video-embed';

const App = () => (
  <CwVideoEmbed thumbnail="https://images.unsplash.com/photo-1611162617213-7d7a39e9b1d7?w=800&h=450&fit=crop" label="Play: Example video">
    <template>
      <iframe
        src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
        title="Example video"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
        allowFullScreen
      />
    </template>
  </CwVideoEmbed>
);

Examples

Aspect Ratio

<cw-video-embed> reserves layout space up front using the aspect-ratio attribute, so activating the embed never causes content to shift. It defaults to 16 / 9; set it to match your video, or to an empty string to let the embed size itself.

<cw-video-embed
  thumbnail="https://images.unsplash.com/photo-1611162617213-7d7a39e9b1d7?w=800&h=450&fit=crop"
  aspect-ratio="4 / 3"
  label="Play: Example video"
  style="max-width: 400px;"
>
  <template>
    <iframe
      src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
      title="Example video"
      allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
      allowfullscreen
    ></iframe>
  </template>
</cw-video-embed>

Captions

Use the caption slot to add a caption below the embed.

A short behind-the-scenes look at how this example was made.
<cw-video-embed thumbnail="https://images.unsplash.com/photo-1611162617213-7d7a39e9b1d7?w=800&h=450&fit=crop" label="Play: Example video">
  <template>
    <iframe
      src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
      title="Example video"
      allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
      allowfullscreen
    ></iframe>
  </template>
  <span slot="caption">A short behind-the-scenes look at how this example was made.</span>
</cw-video-embed>

Activating Programmatically

Call activate() to trigger the embed without a user click — useful for “play on scroll into view” or similar patterns. Listen for cw-activate to know when it happens.


Activate
<cw-video-embed
  id="programmatic-example"
  thumbnail="https://images.unsplash.com/photo-1611162617213-7d7a39e9b1d7?w=800&h=450&fit=crop"
  label="Play: Example video"
>
  <template>
    <iframe
      src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
      title="Example video"
      allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
      allowfullscreen
    ></iframe>
  </template>
</cw-video-embed>
<br />
<cw-button onclick="document.getElementById('programmatic-example').activate()">Activate</cw-button>

<script>
  const embed = document.getElementById('programmatic-example');
  embed.addEventListener('cw-activate', () => console.log('Activated!'));
</script>

Embedding Non-Video Content

Despite the name, <cw-video-embed> works generically with any iframe-embeddable content — it doesn’t parse or validate what’s inside the <template>. A map embed behind a lazy-loading facade is just as valid a use case.

<cw-video-embed
  thumbnail="https://images.unsplash.com/photo-1524661135-423995f22d0b?w=800&h=450&fit=crop"
  label="Load map"
>
  <template>
    <iframe
      src="https://www.openstreetmap.org/export/embed.html?bbox=-0.1%2C51.5%2C0%2C51.51&layer=mapnik"
    ></iframe>
  </template>
</cw-video-embed>

Importing

If you’re using the autoloader or the traditional loader, you can ignore this section. Otherwise, feel free to use any of the following snippets to cherry pick this component.

Script Import Bundler React

To import this component from the CDN using a script tag:

<script type="module" src="https://cdn.jsdelivr.net/npm/@cordwainer/cw-elements@1.6.0/cdn/components/video-embed/video-embed.js"></script>

To import this component from the CDN using a JavaScript import:

import 'https://cdn.jsdelivr.net/npm/@cordwainer/cw-elements@1.6.0/cdn/components/video-embed/video-embed.js';

To import this component using a bundler:

import '@cordwainer/cw-elements/dist/components/video-embed/video-embed.js';

To import this component as a React component:

import CwVideoEmbed from '@cordwainer/cw-elements/dist/react/video-embed';

Slots

Name Description
(default) An inert <template> containing the real embed markup (typically an <iframe>). Its content is cloned into the light DOM the first time the embed is activated, never before — this is what keeps a third-party player’s own JS and tracking scripts from loading until someone actually clicks.
caption Optional caption content shown below the embed.

Learn more about using slots.

Properties

Name Description Reflects Type Default
thumbnail The URL of the poster image shown before the embed is activated. string ''
label An accessible label for the play control, e.g. "Play: Introducing Cordwainer Elements". A generic facade component has no way to know what the embedded video actually is, so provide this whenever the surrounding context doesn’t already make it obvious — falls back to a generic “Play video” otherwise. string ''
aspectRatio
aspect-ratio
The aspect ratio to reserve for the embed before it activates, as a valid CSS aspect-ratio value (e.g. "16 / 9", "4 / 3"). Reserves layout space up front so activating the embed can’t cause content to shift. Set to an empty string to let the embed size itself instead. string '16 / 9'
loading Whether the thumbnail image should load eagerly or lazily. 'eager' | 'lazy' 'lazy'
activated Indicates whether the real embed has been activated. Reflects, so activation state is stylable and inspectable. boolean false
updateComplete A read-only promise that resolves when the component has finished updating.

Learn more about attributes and properties.

Events

Name React Event Description Event Detail
cw-activate onCwActivate Emitted once, the first time the embed is activated (by a user click or a call to activate()). -

Learn more about events.

Methods

Name Description Arguments
activate() Activates the embed: clones the slotted <template>’s content into the light DOM and emits cw-activate. Does nothing if already activated, or if no <template> is present. -

Learn more about methods.

Parts

Name Description
base The component’s base wrapper.
play-button The clickable button shown before activation, wrapping the thumbnail and play icon.
thumbnail The poster image shown before activation.
play-icon The play icon overlaid on the thumbnail.
embed The container the real embed is cloned into after activation.
caption The container wrapping the caption slot.

Learn more about customizing CSS parts.

Dependencies

This component automatically imports the following dependencies.

  • <cw-icon>