Responsive Video Light-box with Lity

YouTube on a phone

This tutorial shows you how to make an elegant & responsive video pop-up light-box. Instead of just using the standard WordPress block to embed a video, we’ll use the Lity JavaScript library to pop the video to a fully responsive video player, and it’ll work with YouTube and self-hosted videos.

You don’t need to add any new plugins to your WordPress site. All you need is a custom child theme so we can add a bit of PHP and JavaScript to make it work.

Video: Hosted on YouTubeA video
Hosted on YouTube
Video: A self-hosted videoA video
A self-hosted video

Basically, we need a PHP file on the server to implement a custom shortcode. We need the open source Lity library (a free download) and we need to create a bit of CSS to polish it off. If you’re not experienced at coding, it might sound like hard work, but…

…all we’re going to do is create a single PHP function, add a couple of CSS definitions, and use the Lity JavaScript library to do all the hard work.

Getting Started

We’ll start by sorting-out our files, and we’ll put a placeholder function in there so we can test things. In your custom child theme’s main folder, create a new file called simple-video-lightbox.php and paste the following into it.


// Block direct access.
defined('WPINC') || die();

function svl_do_shortcode_video($params) {
	// We'll replace this with the proper code later.
	return '<strong>Light-box in here</strong>';
add_shortcode('svl_video', 'svl_do_shortcode_video');

infoOur function begins with “svl_“, which stands for “Simple Video Light-Box”.

We also need a folder where we can add our supporting files, so make a sub-folder in your custom child theme called simple-video-lightbox. Next, go to the Lity website and download the current version. Extract the zip file and go into the “dist” (distributable) folder and you’ll see several files. We only need the minified JS & CSS files, so copy lity.min.css and lity.min.js into the new simple-video-lightbox sub-folder.

Grab a copy of the YouTube play button SVG file, save it into the sub-folder, and rename it to youtube-play-button.svg.

Yoiu’ll also need a standard (non-YouTube) play button. FInd or create an SVG and save it in the subfolder, with the name “standard-play-button.svg

Create an empty CSS file in the sub-folder called svl-frontend.css.

Finally, we need to tell our custom child theme to use the new code, so open your custom child theme’s functions.php file and add the following into it.

list of files for simple video lightbox
Asset files for our Simple Video Light-box project
 * Simple Video Light-box.
require_once dirname(__FILE__) . '/simple-video-lightbox.php';

OK, that should be enough to tell WordPress that we’ve created a new shortcode called svl_video, so let’s see if it works. Edit a page or a post, create a shortcode block and add the new shortcode with a “url” parameter pointing to a YouTube video. Save your content and view it. If everything’s working properly, you should see the text “Light-box in here” rendered when you want your video to be.

video lightbox shortcode
Simple Video Light-box shortcode block

Render the Shortcode’s HTML

Now the “scaffolding” is in place, we can add the code to render the proper HTML. We’re going to create an outer figure container, which will wrap a thumbnail/poster image that we can grab from YouTube (based on the video URL), or we can specify it manually (with the thumb="..." parameter). Update your simple-video-lightbox.php file with the following.


 * WP Tutorials : Simple Video Lightbox (svl)

// Block direct access.
defined('WPINC') || die();

const SVL_LITY_VERSION = '2.4.1';

function svl_enqueue_assets() {
	global $svl_have_assets_been_queued;

	if (is_null($svl_have_assets_been_queued)) {
		$base_url = get_stylesheet_directory_uri() . '/' . pathinfo(__FILE__, PATHINFO_FILENAME) . '/';
		$theme_version = wp_get_theme()->get('Version');

		// Lity
		wp_enqueue_style('lity', $base_url . 'lity.min.css', null, SVL_LITY_VERSION);
		wp_enqueue_script('lity', $base_url . 'lity.min.js', null, SVL_LITY_VERSION, true);

		// Our frontend CSS.
		wp_enqueue_style('svl-frontend', $base_url . 'svl-frontend.css', null, $theme_version);

		$svl_have_assets_been_queued = true;

function svl_do_shortcode_video($atts) {
	$html = '';

	if (is_admin() || wp_doing_ajax()) {
		// Don't do anything.
	} else {
		// Enqueue the assets.

		// Parse the arguments passed from the shortcode.
		$args = shortcode_atts(
				'url' => '',
				'thumb' => '',
				'alt' => 'A video',
				'w' => 400,
				'h' => 300,
				'caption' => '',

		if (empty($args['alt']) && !empty($args['caption'])) {
			$args['alt'] = $args['caption'];

		$video_url = null;
		$css_classes = array('svl-container');

		if (empty($video_url = $args['url'])) {
			// No video URL specified.
		} elseif (strpos($video_url, '') === false) {
			// Non-YouTube video, possibly self-hosted.
		} else {
			// We have a YouTube video URL, so parse the URL to grab the video
			// reference (v=xxxxxx) and maybe try to get the post JPEG for the
			// video too.
			$youtube_video_ref = null;
			$css_classes[] = 'youtube';

			if (!empty($args['thumb'])) {
				// We've set the thumbnail/poster in the "thumb" arg, so we
				// don't need to fetch the poster from YouTube.
			} else {
				$url_parts = parse_url($video_url);

				$query_args = null;
				if (is_array($url_parts) && array_key_exists('query', $url_parts)) {
					parse_str($url_parts['query'], $query_args);

				if (is_array($query_args) && array_key_exists('v', $query_args) && !empty($query_args['v'])) {
					$youtube_video_ref = $query_args['v'];

				if (!empty($youtube_video_ref)) {
					// Point the thumbnail/poster to YouTube,
					$args['thumb'] = sprintf(

		// If we've got a valid video URL, render the HTML.
		if (empty($video_url)) {
			// No video ref found, so do nothing.
			$html .= '<p>ERROR: No video_url</p>';
		} else {
			$html .= sprintf('<figure class="%s">', esc_attr(implode(' ', $css_classes)));

			$html .= sprintf('<a href="%s" title="%s" data-lity>', esc_url($video_url), esc_attr($args['alt']));

			// The thumbnail / poster image.
			$html .= sprintf(
				'<img src="%s" width="%d" height="%d" alt="%s" />',

			$html .= '</a>';

			if (!empty($args['caption'])) {
				// For SEO and the screen-reader.
				$html .= sprintf('<span class="screen-reader-text">Video: %s</span>', esc_html($args['caption']));

				// The on-screen caption
				$html .= sprintf('<figcaption>%s</figcaption>', esc_html($args['caption']));

			$html .= '</figure>'; // .svl-container

	return $html;
add_shortcode('svl_video', 'svl_do_shortcode_video');

This might seem like a bit of a chunk but it breaks-down quite nicely, and there are loads of comments too. The parameters for our shortcode come through in $atts so we need to pull the video URL, and possibly an SEO-friendly image alt tag for our thumbnail image.

  1. Parse $atts to grab the shortcode’s parameters.
  2. Enqueue the CSS/JS for Lity, along with our own CSS.
  3. If we’ve specified a video URL with “” in it, and we have not specified a thumbnail URL…
    1. Try to auto-create a thumbnail image URL based on the YouTube video URL.
  4. If the shortcode hasn’t specified a video URL…
    1. Output an error message.
  5. Else…
    1. Open the <figure> element.
    2. Open the <a> for Lity.
    3. If a caption has been specified…
      1. Output it in a special screen reader <span> element – good for SEO too.
    4. Render the <img> for the thumbnail/poster.
    5. If a caption has been specified…
      1. Render a <figcaption>
    6. Close-off the <figure> element.
  6. Return $html so WordPress can render it for us.

That should be enough to “make it work”. Save your changes, reload your content and you should see your video thumbnail. Click on it – it should open the video light-box and play 😎

What if it Didn’t Work…

  • If it’s a YouTube URL but you don’t see a video thumbnail, check your YouTube URL is correct and that the video reference is specified with “v=xxxxxxxxxx”
  • The thumbnail shows OK, but clicking on it takes you to the YouTube page instead of showing the pop-out light-box.
    • Check your browser’s JavaScript console to see if it was unable to load the lity.min.js file for some reason.

Adding Some Style

We’ve now got a working shortcode for playing YouTube videos, but it’s not obvious that the image is a video – it just looks like an image. So let’s put a Play button over the image with some CSS. Paste the following into your svl-frontend.css file..

 * Simple Video Light-box

.svl-container {
	position:  relative;
	margin-bottom:  1em;

.svl-container > a {
	display:  block;
	border:  1px solid lightgrey;
	box-shadow: 0 0 1.00em rgba( 0, 0, 0, 0.20 );
	border-radius:  0.35em;
	overflow: hidden;

.svl-container > a:hover {
	box-shadow: 0 0 1.00em rgba( 0, 0, 0, 0.40 );

.svl-container > a img {
	display:  block;
	width:  100%;
	object-fit:  cover;

.svl-container > a::before {
	content: url( 'standard-play-button.svg' );
	position:  absolute;
	left:  50%;
	top:  50%;
	width:  25%;
	max-width:  5em;
	transform: translate(-50%, -50%);
	transition: 0.3s;
	opacity: 0.85;
	z-index:  10;
} > a::before {
	content: url( 'youtube-play-button.svg' );

.svl-container > a:hover::before {
	opacity: 1.00;

.svl-container figcaption {
	text-align: center;
	position:  absolute;
	bottom:  0;
	width:  100%;
	padding: 0.5em 0;
	color:  white;
	background-color:  #444444c0;
	pointer-events: none;

The CSS is pretty straightforward – easy to hack it about to suit your needs.

Wrapping it Up

I think it’s nicer to have videos embedded like this, rather than with the oEmbed YouTube stuff, because it avoids using an iFrame. iFrames were a bad idea back in the 2000s, and they’re still a bad idea now. So there.

Now that you’ve got 100% control of the HTML, you can really milk it for SEO. Try extending this code a bit …

  • Set the aria-label property of the <a> element for better accessibility.
  • Sprinkle some custom JavaScript mojo over it to render some cool mouse-over effects.
  • Check out the Lity website for more info too, because it’s not just limited to showing videos.

Have fun adding video thumbnails all over your WordPress site! 👍

Like This Tutorial?

Let us know

WordPress plugins for developers

7 thoughts on “Responsive Video Light-box with Lity”

  1. Hi there, great tutorial. Worked great except one thing. I don’t use a child theme, and was wondering if I put the files in a different directory, how to call it.

  2. Hey Folks – This is going to be a nice fit for doing what I want – thanks a BUNCH!!!

    My goal is to streamline things a bit though, and perhaps you could lend a hand. The goals would be the following:

    1. Eliminate the child theme sub-folder. Would be a lot more transportable having all the code contained in either a snippets manager (CodeSnippetsPro, WP Codebox, etc) or in the child theme functions.php and style.css.
    2. Enqueue the Lity libraries from a CDN. This is a pretty easy declaration in an HTML snippet.

    The big bang for the buck in my view is that the capability is easily transportable, fits very nicely into a starter site and eliminates all the manual file manipulations. Would appreciate your thoughts on this approach and how you would set about handling these changes.

    • Hi David

      If you want to make things more portable between your WP projects, the easiest thing to do is convert the tutorial into a small plugin to hold the PHP, CSS and the Lity JS code.

      Personally, I would avoid putting the project’s CSS in the Customiser. If you do that, you’re enqueuing the CSS on every page-load across the entire site, regardless of whether there’s a Lity video on there or not. It works, but it’s a bit lazy and you’ll start seeing warnings about “unused CSS” when you test the site’s page speed.

      I would also avoid offloading the Lity library to a CDN. If you keep it self-hosted you can use Autoptimize (or similar) to aggregate each page’s CSS & JS assets for you. This results in the browser having to make fewer requests on each page-load and will give you a better speed-boost than offloading such a small library to a CDN.

      A nice modification might be to change how the play button SVGs are rendered. Moving them to inline SVGs would keep things clean.

      Because the tutorial is fairly lightweight already, I think the biggest optimisation you can look at is SEO optimisation. If you were to inject VideoObject structured data alongside the HTML, the page would get more search exposure. You could even automate fetching the VideoObject meta (description and publication-date) from YouTube. That would be cool.

      I hope this helps

      • Good Day Headwall –
        Thanks for the quick and informative response. I got a good night’s sleep and tackled some more this morning with a fresh perspective, especially considering your advisements. So, here’s an update. First of all, I have everything working correctly. It does make a difference when I follow the instructions completely…. I did have some files in the wrong location but got that all straightened out. I also had multiple enqueue events for the Lity libraries…. By nature, I tend to tinker with things to learn more about how they work, and tinker I did.

        I will be incorporating the capability into my starter site which replicates nicely. As I improve this capability, it’s an easy update. I found that EmbedPlus instantiates the Lity libraries, so I commented the enqueue sections out in your PHP. If not using EmbedPlus, it’s a simple update. I appreciate the advice of locally hosting the Lity libraries – solid approach.

        For the CSS, I’ll work it in the Customizer during the dev stages and then push it to the svl-fronted.css. Also incorporating some of the Lity CSS to enhance the lightbox display to my needs and will use the same approach. I use Perfmatters and we’ll see how it picks up on the optimizations. The site I’m working on is video intensive with a video on just about every post. I can see on other site designs where this would really make sense.

        Good advice on the SEO stuff too. I’ve tagged the VideoObject injection on my task backlog.

        Nice touch on the screen reader picking up the caption – so many developers tend to leave this kind of stuff out.

        In your shortcode I noted the parsing of arguments. I’m assuming all of those shortcode_atts can be declared in the shortcode? Here’s my take – please correct me if wrong. (Note: I use ACF extensively and have a tags snippet that I enclose {{field.field_name}} which works quite nicely. If my assumptions below are correct, I’ll build out the ACF tags.)

        [svl_video url="{{field.youtube_url}}" thumb="" alt="SEO friendly alt-tag" w="800" h="600" caption="{{field.svl_caption}}"]

        Looks like you invoke Lity on the anchor tag inside a figure element. The implementation is clean and simple – kudos. I’m sifting through the EmbedPlus code to see how I can add on the Lity implementation with that player – but it’s going on my task backlog now – I have something that’s nice and works well – thank you sincerely.

        • ooh yes, it looks like I’ve got the screen-reader-text in there twice. I’ll fix that – it only needs to be in there once.

          The shortcode_atts() function is worth learning as it’s quite easy. It’s just a set of keys and default-values, so you use things like this for the default values…


          …you can call the shortcode without any parameters. Of course you can override the defaults by passing parameters in the shortcode, but using ACF/meta to set the shortcode defaults will keep your shortcodes really clean.

          It sounds like you’re making good progress 👍

  3. Grabbed a cup of java, and offering some additional comments. I realize my previous comment is still in the moderation category.

    I dropped the CSS into the customizer (or the child theme), and it works nicely there – significantly easier to style on the fly. Notably, the CSS when loaded to the server side only did not work. Also loaded the svg assets to the media library – going to have to work on the CSS for sizing and location tho.

    I’m working on implementing the EmbedPlus YouTube Pro player in lieu of the default Gutenberg player. The options available are considerable with this player, including modest branding that actually works!!! The iframe code generation rendered on the page is significant. Any pointers on where to start in that endeavor would be appreciated.


Leave a comment