A normal YouTube embed in WordPress loads the full player, its scripts and YouTube cookies on every page view, even when nobody presses play. On a page with a few videos that is the heaviest part of the page.
This shortcode shows only the video thumbnail with a play button. The player is created when the visitor clicks, from youtube-nocookie.com, and it starts playing right away. Use it in any post, page or Shortcode block:
[yt id=0-wlEr669CY](the old usage keeps working)[yt id="0-wlEr669CY" title="Product tour" start="30"]
The snippet
<?php
/**
* [yt id="VIDEO_ID" title="Video title" start="30"]
* Prints a thumbnail with a play button. The youtube-nocookie.com player
* is only loaded when the visitor clicks it.
*/
add_shortcode( 'yt', 'dpc_youtube_facade_shortcode' );
function dpc_youtube_facade_shortcode( $atts ): string {
$atts = shortcode_atts(
array(
'id' => '',
'title' => __( 'YouTube video', 'dpc' ),
'start' => 0,
),
$atts,
'yt'
);
$id = trim( (string) $atts['id'] );
if ( ! preg_match( '/^[A-Za-z0-9_-]{11}$/', $id ) ) {
// Only people who can edit content see why nothing rendered.
return current_user_can( 'edit_posts' )
? '<p class="dpc-yt-error">' . esc_html__( 'YouTube shortcode: the id must be the 11-character video ID.', 'dpc' ) . '</p>'
: '';
}
$title = sanitize_text_field( $atts['title'] );
$start = absint( $atts['start'] );
dpc_youtube_facade_assets();
return sprintf(
'<div class="dpc-yt"><button type="button" class="dpc-yt__play" data-dpc-yt-id="%1$s" data-dpc-yt-start="%2$d" data-dpc-yt-title="%3$s" title="%4$s" aria-label="%4$s"><img src="%5$s" alt="" width="480" height="360" loading="lazy" decoding="async"><span class="dpc-yt__icon" aria-hidden="true"></span></button></div>',
esc_attr( $id ),
$start,
esc_attr( $title ),
/* translators: %s: video title. */
esc_attr( sprintf( __( 'Play video: %s', 'dpc' ), $title ) ),
esc_url( 'https://i.ytimg.com/vi/' . $id . '/hqdefault.jpg' )
);
}
/** CSS and JS once per page, and only on pages that contain a video. */
function dpc_youtube_facade_assets(): void {
if ( wp_script_is( 'dpc-yt', 'enqueued' ) ) {
return;
}
$css = '.dpc-yt{position:relative;aspect-ratio:16/9;overflow:hidden;background:#000}'
. '.dpc-yt__play,.dpc-yt iframe{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;border:0}'
. '.dpc-yt__play{cursor:pointer;background:#000}'
. '.dpc-yt__play img{display:block;width:100%;height:100%;object-fit:cover}'
. '.dpc-yt__icon{position:absolute;top:50%;left:50%;width:68px;height:48px;border-radius:12px;background:rgba(0,0,0,.75);transform:translate(-50%,-50%);transition:background .2s}'
. '.dpc-yt__icon::before{content:"";position:absolute;top:50%;left:50%;border-style:solid;border-width:11px 0 11px 19px;border-color:transparent transparent transparent #fff;transform:translate(-40%,-50%)}'
. '.dpc-yt__play:hover .dpc-yt__icon,.dpc-yt__play:focus-visible .dpc-yt__icon{background:#c00}'
. '.dpc-yt__play:focus-visible{outline:3px solid #fff;outline-offset:-6px}';
$js = <<<'JS'
document.addEventListener('click', function (event) {
var button = event.target.closest('.dpc-yt__play');
if (!button) {
return;
}
var id = button.getAttribute('data-dpc-yt-id') || '';
if (!/^[A-Za-z0-9_-]{11}$/.test(id)) {
return;
}
var start = parseInt(button.getAttribute('data-dpc-yt-start'), 10) || 0;
var iframe = document.createElement('iframe');
iframe.src = 'https://www.youtube-nocookie.com/embed/' + id + '?autoplay=1' + (start > 0 ? '&start=' + start : '');
iframe.title = button.getAttribute('data-dpc-yt-title') || '';
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share';
iframe.allowFullscreen = true;
iframe.referrerPolicy = 'strict-origin-when-cross-origin';
button.replaceWith(iframe);
iframe.focus();
});
JS;
// Handles without a file: WordPress prints only the inline code.
wp_register_style( 'dpc-yt', false, array(), null );
wp_add_inline_style( 'dpc-yt', $css );
wp_enqueue_style( 'dpc-yt' );
wp_register_script( 'dpc-yt', false, array(), null, array( 'in_footer' => true ) );
wp_add_inline_script( 'dpc-yt', $js );
wp_enqueue_script( 'dpc-yt' );
}
Add it with Scripts Organizer
In Scripts Organizer go to Code Blocks → Add New and give the block a name. Set Trigger location to Everywhere, tick PHP under Script location, paste the code into the PHP editor and publish. Leave Action/Hook empty.
Everything lives in that one PHP block on purpose. Scripts Organizer prints Header and Footer code only with the Conditions trigger, so a separate CSS or JS part would either need its own conditions or load on every page. Here the shortcode enqueues its small inline style and script itself, once per page and only on pages that actually contain a video. current_user_can() is only called inside the shortcode, after WordPress has loaded the user.
The same code also works unchanged in a small plugin or an mu-plugin.
How it works
- The
idmust be an 11-character YouTube ID (A-Z,a-z,0-9,_,-). Anything else renders nothing for visitors, and a short notice for users who can edit posts. Every attribute is escaped on output, andextract()is gone. - The facade is a real
<button>with anaria-labelandtitle, so keyboard and screen reader users can start the video. The thumbnail hasloading="lazy"and explicit dimensions. - One click listener on the document handles every video on the page. It checks the ID again, builds the
youtube-nocookie.com/embed/iframe withautoplay=1and the optionalstart, and replaces the button with it. No jQuery. - CSS and JS are registered as handles without a file, so WordPress prints only the inline code: the style wherever styles are still being printed, the script in the footer.
wp_script_is()makes sure they are added once, however many videos the page has. aspect-ratio: 16/9keeps the box responsive, so the separate wrapper CSS from the old version is no longer needed.
Customise it
- Sharper thumbnails: replace
hqdefault.jpgwithmaxresdefault.jpg. Not every video has that size, so test yours. - Other colours: change
#c00(hover) andrgba(0,0,0,.75)(button) in$css. - If another plugin already registers a
[yt]shortcode, change the first argument ofadd_shortcode()and the third argument ofshortcode_atts().
Gotchas
- The thumbnail still comes from
i.ytimg.com, so the visitor’s browser contacts YouTube before the click. For strict consent setups, host the thumbnail yourself. - Browsers may ignore autoplay with sound. On a click it is usually allowed, but some mobile browsers need a second tap.
youtube-nocookie.comavoids tracking cookies until playback starts; it is not a full consent solution.- Page builders that render shortcodes in an editor preview will show the facade there too, which is expected.
In the Scripts Organizer library Import it with one click from Scripts Organizer.