Lazy-loading YouTube facade
Show a thumbnail and play button instead of an iframe, and swap in the real embed only when the visitor clicks.
WordPressUtilitiesyoutubelazy-loadingperformanceembed
Live preview
Code
/**
* Replaces [data-yt] buttons with a youtube-nocookie iframe on first click.
* Markup: <button type="button" class="yt-facade" data-yt="VIDEO_ID" data-title="Video title"></button>
*/
(() => {
const ID = /^[\w-]{11}$/; // YouTube ids are 11 safe characters.
function mount(btn) {
const id = btn.dataset.yt;
if (!ID.test(id || '')) return;
const title = btn.dataset.title || 'Video';
btn.style.backgroundImage = `url(https://i.ytimg.com/vi/${id}/hqdefault.jpg)`;
btn.setAttribute('aria-label', `Play video: ${title}`);
btn.addEventListener('click', () => {
const frame = document.createElement('iframe');
frame.src = `https://www.youtube-nocookie.com/embed/${id}?autoplay=1&rel=0`;
frame.title = title;
frame.allow = 'autoplay; encrypted-media; picture-in-picture';
frame.allowFullscreen = true;
frame.loading = 'lazy';
btn.replaceWith(frame);
frame.focus();
}, { once: true });
}
// Only set up thumbnails near the viewport.
const io = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) { mount(entry.target); io.unobserve(entry.target); }
});
}, { rootMargin: '200px' });
document.querySelectorAll('.yt-facade[data-yt]').forEach((el) => io.observe(el));
})();
.yt-facade { position: relative; display: block; width: 100%; aspect-ratio: 16 / 9; border: 0; padding: 0; cursor: pointer; background: #000 center / cover no-repeat; }
.yt-facade::after { content: ""; position: absolute; inset: 50% auto auto 50%; width: 4.5rem; height: 3.2rem; transform: translate(-50%, -50%); border-radius: 1rem; background: #e00; }
.yt-facade::before { content: ""; position: absolute; z-index: 1; inset: 50% auto auto 50%; transform: translate(-30%, -50%); border-style: solid; border-width: .7rem 0 .7rem 1.2rem; border-color: transparent transparent transparent #fff; }
.yt-facade:focus-visible { outline: 3px solid #fff; outline-offset: -6px; }
iframe[src*="youtube-nocookie"] { width: 100%; aspect-ratio: 16 / 9; border: 0; }
How it works
What it solves
A single YouTube iframe downloads several hundred kilobytes of script before anyone presses play, and it sets tracking cookies. A facade shows a lightweight image and loads the real player only on demand, which helps page speed and privacy.
How it works
Each embed is a button with a video id. When it approaches the viewport, an observer sets its thumbnail and an accessible label. The id is checked against an eleven character pattern so a crafted attribute cannot inject a URL. On click, the button is replaced by an iframe on the privacy-enhanced domain with autoplay, a title and the correct permissions, and focus moves to it.
CSS draws the play icon with pseudo-elements, so no image is needed.
Adapting it
In WordPress, filter embed_oembed_html to turn YouTube iframes into these buttons, or output them from a shortcode. Add a data-start option by appending start= to the URL after validating it as an integer. Use maxresdefault.jpg for high resolution thumbnails when you know they exist.
Pitfalls
- Without JavaScript the button does nothing; consider a visible link to the video as fallback.
- The thumbnail is a request to a third party, so mention it in privacy notes.
- Keep the aspect ratio on the wrapper to avoid layout shift.
- Autoplay works because the click is a user gesture.
How to use it
Include both files on pages with videos.
Add <button type="button" class="yt-facade" data-yt="abcdefghijk" data-title="Intro"></button> where the video should be.
Test keyboard activation with Tab and Enter.
Licence: GPL-2.0-or-later. This snippet comes from WordPress code and is GPL-2.0-or-later.