YouTube and Vimeo Web Component

If you want something done right, post it wrong.

In the long history of the innertubes, if you ask for help with code you typically won’t get much of a response. But if you post code and assert it is ideal and perfect and an immutable reflection of your pristine ego, then folks will give it a good kick.

The Why

The water closet emoji overlaid with two wrenches borrowed from an early web components logo.
I created this web components logo using insight from Mayank (who pre-emptively reminds me this is a custom element) and Zach Leatherman.

In Embed Slides, YouTube Videos, and More I explain that making your own embed for YouTube spares users from downloading a half megabyte of tracking scripts (at least until they play the video).

I made a web component version of it as well, but noted that it would probably need some help. I had wanted to include a Vimeo version within the component, but struggled with the Vimeo thumbnail.

Instead of burning more time running at something I was clearly bad at, I figured I could share what I have and the community could ignore it as they see fit.

The HTML

I structured the HTML for the web component so that if the JavaScript never loads or breaks or fails in some exciting way, the user will still have a valid link and context.

For it to work, however, requires a tiny bit of effort on the part of authors to ensure the link is formatted a specific way. Mostly because the unique ID, the slug, at the end of both the YouTube and Vimeo URLs is what I use to grab the poster image and build the embedded frame.

The HTML within the custom element is retained, helping ensure if the embed itself is broken or blocked, even after the script successfully runs, the user will still have a valid link and context.

<youtube-vimeo-embed>
  <p>
    YouTube: <a href="https://youtu.be/dQw4w9WgXcQ">Rick Astley, "Never Gonna Give You Up"</a>, 3:32
  </p>
</youtube-vimeo-embed>
<youtube-vimeo-embed>
  <p>
   Vimeo: <a href="https://vimeo.com/77377158">Key &amp; Peele: Video Chat</a>, 2:12
  </p>
</youtube-vimeo-embed>

You can pass in the aspect ratio. I set aspect-ratio:16/9 by default, but if you add data-ratio to the <youtube-vimeo-embed> you can pass in your own value for the property.

Thanks to a comment shortly after posting, I have also added support for data-poster. Give it a full URL and, if provided, there’s no call to YouTube or Vimeo, thus saving bandwidth and not providing a beacon to YouTube. Otherwise it pulls the image from YouTube or the Vumbnail.

The Script

The function parses the URL and uses that to decide if the video is YouTube or Vimeo. You have to use the youtu.be domain.

Then I create an iframe and set the attributes I want, pulling the title from the text you, the HTML author, wrote.

For YouTube I use the no-cookie embed in a vague nod to privacy (it’s still YouTube) and pull the thumbnails it uses for videos. Vimeo is a bit trickier, since I would need to make an API call for the thumbnail, so I use vumbnail.com.

If you have query string values, the function appends them to the end of the URL, after the ?autoplay=1. I did not do a deep dive into all possible query string values to know how they may impact display, so adjustments may be needed for your use case.

class YouTubeVimeoEmbed extends HTMLElement {
	// https://github.com/aardrian/youtube-vimeo-embed
	constructor() {
		super();
	}
	connectedCallback() {
		var iframe = document.createElement('iframe');
		var theLink = this.querySelector('a');
		var vidPoster = this.getAttribute('data-poster');
		var vidSlug;
		var vidPreSlug = theLink.getAttribute('href').split('/')[3];
		var vidQueryString = '';
		var vidQueryCheck = vidPreSlug.indexOf('?');
		if (vidQueryCheck == -1) {
			vidSlug = vidPreSlug;
		} else {
			vidSlug = vidPreSlug.substring(0,vidQueryCheck);
			vidQueryString = '&'+vidPreSlug.substring(vidQueryCheck+1);
		}
		if (theLink.getAttribute('href').substring(8,13) == 'youtu') {
			var isYouTube = 1;
		} else {
			var isYouTube = 0;
		}
		iframe.setAttribute('title',theLink.textContent);
		iframe.setAttribute('allow','autoplay');
		iframe.setAttribute('allowfullscreen','');
		iframe.setAttribute('loading','lazy');
		if (isYouTube) {
			iframe.setAttribute('src','https://www.youtube-nocookie.com/embed/'+vidSlug);
			var vidLink = 'https://www.youtube-nocookie.com/embed/'+vidSlug+'?autoplay=1'+vidQueryString;
			if (vidPoster) {
				var bgImg = vidPoster;
			} else {
				var bgImg = 'https://i3.ytimg.com/vi/'+vidSlug+'/hqdefault.jpg';
			}
		} else {
			iframe.setAttribute('src','https://vimeo.com/'+vidSlug);
			var vidLink = 'https://player.vimeo.com/video/'+vidSlug+'?autoplay=1&dnt=1'+vidQueryString;
			if (vidPoster) {
			  var bgImg = vidPoster;
			} else {
			  var bgImg = 'https://vumbnail.com/'+vidSlug+'.jpg';
			}
		}
		if (this.getAttribute('data-ratio')) {
			iframe.setAttribute('style','aspect-ratio:'+this.getAttribute('data-ratio')+';width:100%;');
		} else {
			iframe.setAttribute('style','aspect-ratio:16/9;width:100%;');
		}
		iframe.setAttribute('srcdoc','<style>body{background-image:url('+bgImg+');background-repeat:no-repeat;background-size:cover;background-position:center center;display:grid;place-items:center;min-height:97dvh;overflow:hidden;}a{display:block;width:96px;height:96px;overflow:hidden;}a:focus{outline:none;}a:focus circle,a:hover circle{fill:#000;}a:focus circle:first-child + circle,a:hover circle:first-child + circle{stroke-dasharray:.4,.4;}a:focus polygon,a:hover polygon{stroke:#fff;stroke-width:.75;}</style><a href="'+vidLink+'"><svg viewBox="0 0 16 16" width="96" height="96" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><circle cx="50%" cy="50%" r="7.75" fill="none" stroke="#000" stroke-width=".5"/><circle cx="50%" cy="50%" r="7.25" fill="none" stroke="#fff" stroke-width=".5"/><circle cx="50%" cy="50%" r="7" fill="#0009"/><polygon points="12, 8 6, 4.5 6, 11.5" fill="#fff" stroke-linejoin="round"></polygon></svg>Play</a>');
		this.appendChild(iframe);
	}
}
customElements.define('youtube-vimeo-embed', YouTubeVimeoEmbed);

Note that I do not blow away the original link and surrounding text. It serves as its own fallback.

The Examples

The classic RickRoll from YouTube, but here you know it’s coming.

<youtube-vimeo-embed>
  <p>
    YouTube: <a href="https://youtu.be/dQw4w9WgXcQ">Rick Astley, "Never Gonna Give You Up"</a>, 3:32
  </p>
</youtube-vimeo-embed>

YouTube: Rick Astley, “Never Gonna Give You Up”, 3:32

A Vimeo that seems to capture the experience we all have in non-IRL meetings today.

<youtube-vimeo-embed>
  <p>
   Vimeo: <a href="https://vimeo.com/77377158">Key & Peele: Video Chat</a>, 2:12
  </p>
</youtube-vimeo-embed>

Vimeo: Key & Peele: Video Chat, 2:12

An “ultrawide” video from YouTube using data-ratio="21/9". The poster image was created by the author at the wrong size; the crop is unavoidable even if not ideal.

<youtube-vimeo-embed data-ratio="21/9">
  <p>
    YouTube: <a href="https://youtu.be/DsoZI4TLqMc">The Boy and the Heron trailer</a>, 1:50
  </p>
</youtube-vimeo-embed>

YouTube: The Boy and the Heron trailer, 1:50

Here’s a square video from Vimeo where I set data-ratio="1/1". If you use this on your site, you may want to have your own styles to limit the iframe to height 90vh (or whatever works for your site) and lean on the aspect ratio. I opted not to be so prescriptive here.

<youtube-vimeo-embed data-ratio="1/1">
  <p>
    Vimeo: <a href="https://vimeo.com/31817442">No Undo - Christophe Dillinger</a>, 2:14
  </p>
</youtube-vimeo-embed>

Vimeo: No Undo – Christophe Dillinger, 2:14

Oh hey, here’s an expert debunking accessibility overlays. You know, in case you wanted to use a test video to embed in your project. It starts 31 seconds into the video thanks to start=31 in the query string.

<youtube-vimeo-embed>
  <p>
    YouTube: <a href="https://youtu.be/PLXAuxZKKjs?start=31">Overlays Underwhelm at WordPress Accessibility Day</a>, 46:48
  </p>
</youtube-vimeo-embed>

YouTube: Overlays Underwhelm at WordPress Accessibility Day, 46:48

This example starts 14 seconds in, uses a custom poster image (via data-poster), and sets the aspect ratio to 1:1 because my ego insists the poster image is more compelling:

<youtube-vimeo-embed data-poster="https://adrianroselli.com/wp-content/uploads/2024/06/web-components-logo.png" data-ratio="1/1">
  <p>
   <a href="https://youtu.be/Sq5oiHjwFxI?start=14">Kevin Powell: Creating Web Components with Dave Rupert</a>, lasting over an hour and a half.
  </p>
</youtube-vimeo-embed>

Kevin Powell: Creating Web Components with Dave Rupert, lasting over an hour and a half.

The GitHub Repo

This is perfect and you are all too embarrassed to pretend otherwise. You can prove it by ignoring the GitHub repo for <youtube-vimeo-embed>.

5 Comments

Reply

Why didn’t you add posibility to set custom thumbnail then?

In response to dontliem1. Reply

Didn’t think of it.

I can set up a data-poster attribute. Should be easy enough and is similar to the poster attribute used by <video>.

In response to dontliem1. Reply

Done!

Reply

After iframe is loaded focus is lost. It is going to the first focusable element on the page.

roisag; . Permalink
In response to roisag. Reply

Roisag, can you tell me the browser / OS (and versions)? That will make it easier for me to reproduce and test. I made an issue so I can track it (and hopefully fix it).

Leave a Comment or Response

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>