Responsive video embed
YouTube and Vimeo that stay 16:9, with a facade that saves about a megabyte per page.
<!-- 1. plain responsive embed -->
<div class="video">
<iframe
src="https://www.youtube-nocookie.com/embed/aqz-KE-bpKQ"
title="Big Buck Bunny — animated short film"
loading="lazy"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen></iframe>
</div>
<!-- 2. facade: a thumbnail and a play button, swapped for the player on click -->
<h3>Click-to-play facade</h3>
<div class="video facade" id="v2" data-id="aqz-KE-bpKQ"
data-title="Big Buck Bunny — animated short film">
<button type="button" class="play" aria-label="Play: Big Buck Bunny — animated short film">
<img src="https://i.ytimg.com/vi/aqz-KE-bpKQ/hqdefault.jpg" alt="" loading="lazy" width="480" height="360">
<span class="btn" aria-hidden="true">
<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
</span>
</button>
</div>
<p class="under">The facade loads one image. The player only arrives if someone presses play.</p>* { box-sizing: border-box; margin: 0; }
body { font: 16px/1.6 system-ui, -apple-system, sans-serif; background: #f6f8fb; padding: 22px; }
h3 { font-size: 1rem; color: #16202e; margin: 26px 0 10px; }
.under { font-size: .85rem; color: #8593ab; margin-top: 10px; }
/* ============ the responsive wrapper ============ */
.video {
position: relative;
aspect-ratio: 16 / 9;
border-radius: 12px;
overflow: hidden;
background: #0f1622;
}
.video iframe {
width: 100%;
height: 100%;
border: 0;
display: block;
}
/* ================================================ */
/* ---- facade ---- */
.facade .play {
position: absolute;
inset: 0;
width: 100%;
padding: 0;
border: 0;
background: none;
cursor: pointer;
display: block;
}
.facade img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.facade .btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 68px;
height: 48px;
display: grid;
place-items: center;
border-radius: 12px;
background: rgba(15, 22, 34, .78);
transition: background .16s ease, transform .16s ease;
}
.facade .play:hover .btn { background: #c93b3b; transform: translate(-50%, -50%) scale(1.06); }
.facade .play:focus-visible { outline: 3px solid #3b4fe4; outline-offset: -3px; }
.facade .btn svg { width: 26px; height: 26px; fill: #fff; }
@media (prefers-reduced-motion: reduce) {
.facade .btn { transition: none; }
.facade .play:hover .btn { transform: translate(-50%, -50%); }
}document.querySelectorAll(".facade").forEach((box) => {
box.querySelector(".play").addEventListener("click", () => {
const frame = document.createElement("iframe");
// autoplay=1 is what makes the click feel like it played the video
frame.src = `https://www.youtube-nocookie.com/embed/${box.dataset.id}?autoplay=1&rel=0`;
frame.title = box.dataset.title;
frame.allow = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share";
frame.referrerPolicy = "strict-origin-when-cross-origin";
frame.allowFullscreen = true;
box.replaceChildren(frame);
frame.focus();
});
});How it works
A YouTube embed is expensive. The standard iframe pulls somewhere around a megabyte of script and makes a dozen network requests before anyone has decided to watch anything. On a page with three videos that is most of your performance budget spent on content nobody may look at.
The facade fixes it. Load the thumbnail — one image, around 15 KB — and a play button. Swap in the real iframe on click, with autoplay=1 so the press feels like it started the video rather than merely loading a player. This is the single biggest win available on any page with video on it.
youtube-nocookie.com is YouTube's privacy-enhanced domain. It still logs the request, but it does not set tracking cookies until playback starts. Use it as the default; there is no downside.
aspect-ratio, not the padding hack. aspect-ratio: 16 / 9 on the wrapper and 100% width and height on the iframe. The old padding-bottom: 56.25% technique still works but nobody can remember what the number means.
The thumbnail has empty alt text. The button around it carries an aria-label that names the video, so alt text on the image would repeat it. hqdefault.jpg is 480×360 and exists for every video; maxresdefault.jpg is sharper but is missing on older uploads and returns a grey placeholder.
Accessibility notes
The title on the iframe is what a screen reader announces instead of "frame". Name the video, not the platform.
The facade is a real <button>, so it is in the tab order and activates on Enter and Space. Its aria-label starts with "Play:" so the action is clear before the title.
Autoplay only ever fires as the result of a deliberate click here. Video that starts on page load is disorienting for people with vestibular or attention conditions, and browsers block it with sound anyway.
Provide captions on the video itself. An embed cannot add them, and for most audiences captions matter more than anything on this page.
Making it yours
Vimeo: same wrapper, https://player.vimeo.com/video/ID as the source. Vimeo's thumbnails need an API call, so for a facade you are better off saving the image yourself.
Self-hosted video: drop the iframe entirely and use <video controls preload="metadata" poster="cover.jpg">. preload="metadata" fetches a few kilobytes rather than the whole file, and the poster is what people see until they press play.
Other ratios: vertical video is 9 / 16, older material is 4 / 3. Change one value.
rel=0 no longer stops related videos appearing — it now limits them to the same channel, which is the closest thing available.
Related templates
Google Maps embed
A responsive map with a title, lazy loading and an optional click-to-load consent gate.
HTMLCSSJavaScriptStyled audio player
A custom player over the native audio element — keyboard accessible, no library.
HTMLCSSJavaScriptSocial share buttons
Plain links to each network's share URL, plus the native share sheet where it exists.
HTMLCSSJavaScriptCheck your version
Once you have edited this, the HTML validator will catch any tag you left unclosed, and the formatter will tidy the indentation. Both run in your browser.