Paste a YouTube iframe into a Shopify section and every visitor downloads YouTube's player before they've decided to watch anything. On a homepage, that's often the heaviest thing on the page, and most people never press play.
The fix is old and simple: show a cover image and a play button, and only create the iframe when someone clicks. This post is a complete section you can drop into any Online Store 2.0 theme.
First, check whether you need this
Dawn's own Video section already does this. It wraps the iframe in a <template> inside a deferred-media element and only loads it when the poster is clicked. So if you're on Dawn or a theme built from it and you used the built-in Video section, you're fine.
Where I keep finding the problem:
- a raw iframe pasted into a Custom Liquid section
- an older theme with its own video section
- a custom section someone built for a product story
Quick test: open DevTools, go to the Network tab, type youtube in the filter and reload the page. If requests show up before you've clicked anything, keep reading.
The section
Create sections/video-on-click.liquid:
{%- liquid assign video = section.settings.video_url assign label = section.settings.title | default: 'Play video' -%} {%- if video != blank -%} <div class="video-on-click" data-video-id="{{ video.id }}" data-video-type="{{ video.type }}"> <button type="button" class="video-on-click__poster" aria-label="{{ label | escape }}"> {%- if section.settings.cover != blank -%} {{ section.settings.cover | image_url: width: 1600 | image_tag: loading: 'lazy', sizes: '100vw', alt: '' }} {%- elsif video.type == 'youtube' -%} <img src="https://i.ytimg.com/vi/{{ video.id }}/hqdefault.jpg" width="480" height="360" loading="lazy" alt=""> {%- endif -%} <span class="video-on-click__play" aria-hidden="true"></span> </button> </div> {%- endif -%} {% stylesheet %} .video-on-click { position: relative; aspect-ratio: 16 / 9; background: #000; } .video-on-click__poster { all: unset; cursor: pointer; display: block; width: 100%; height: 100%; } .video-on-click__poster img, .video-on-click iframe { width: 100%; height: 100%; object-fit: cover; border: 0; display: block; } .video-on-click__play { position: absolute; inset: 0; margin: auto; width: 72px; height: 72px; border-radius: 50%; background: rgba(0, 0, 0, .65); } .video-on-click__play::after { content: ""; position: absolute; left: 29px; top: 22px; border-style: solid; border-width: 14px 0 14px 22px; border-color: transparent transparent transparent #fff; } .video-on-click__poster:focus-visible { outline: 3px solid #fff; outline-offset: -3px; } {% endstylesheet %} {% javascript %} document.addEventListener('click', (event) => { const button = event.target.closest('.video-on-click__poster'); if (!button) return; const box = button.parentElement; const id = box.dataset.videoId; const src = box.dataset.videoType === 'vimeo' ? `https://player.vimeo.com/video/${id}?autoplay=1` : `https://www.youtube-nocookie.com/embed/${id}?autoplay=1`; const iframe = document.createElement('iframe'); iframe.src = src; iframe.title = button.getAttribute('aria-label'); iframe.allow = 'autoplay; encrypted-media; picture-in-picture; fullscreen'; iframe.allowFullscreen = true; box.replaceChildren(iframe); iframe.focus(); }); {% endjavascript %} {% schema %} { "name": "Video (click to load)", "settings": [ { "type": "video_url", "id": "video_url", "label": "Video URL", "accept": ["youtube", "vimeo"] }, { "type": "image_picker", "id": "cover", "label": "Cover image" }, { "type": "text", "id": "title", "label": "Button label (for screen readers)", "default": "Play video" } ], "presets": [{ "name": "Video (click to load)" }] } {% endschema %} Why it's built this way
The video_url setting does the parsing. Paste a normal YouTube or Vimeo link in the theme editor and Liquid gives you video.id and video.type. No regex on URLs.
The JavaScript lives in {% javascript %}. Shopify outputs that block once per section type, not once per instance. If you put a plain <script> in the section and a merchant adds the section twice, you get two click listeners, and the second one runs after the first has already swapped the button out. One delegated listener avoids all of that.
A real <button>. Keyboard users can tab to it and press Enter, and the label is read out. After the swap, focus moves to the iframe so they aren't dropped back at the top of the page.
The cover image is the merchant's choice. If they upload one, image_tag gives you a srcset for free. If they don't, YouTube's hqdefault.jpg thumbnail is the fallback. It exists for every video, unlike maxresdefault.jpg, which is sometimes missing. Vimeo has no URL pattern for thumbnails like that, so for Vimeo videos ask for a cover image.
Lazy cover, unless it's the hero. If this section sits at the very top of the homepage, the cover image is probably your largest paint. In that case don't lazy-load it: swap loading: 'lazy' for a check on section.index, the same way you would for a hero banner.
youtube-nocookie.com. YouTube's privacy-enhanced embed domain. It plays the same video. Swap it back to www.youtube.com if you rely on anything that needs the standard player.
One trade-off
On mobile, the click creates the iframe with autoplay=1, but some browsers still want a second tap before the video plays. That's the price of not loading the player up front. I'll take one extra tap for the few people who watch over a slower homepage for everyone who doesn't.
Check it worked
Same test as before: Network tab, filter youtube, reload. Nothing should load until you click the cover. Then click and the player should start.
If you'd like a second pair of eyes on a slow theme, that's most of what we do: https://rizehub.ph/services/shopify-theme-development


