Elementor Static Gallery

Elementor Pro provides several gallery options out of the box, but I ran into a few consistent issues with all of them. First, the thumbnail images were loaded via JavaScript rather than standard <img> tags, which caused visible delays before they appeared — especially on slower connections. Second, Elementor’s default lightbox relies on the eicons font file for its navigation and close icons. Since I prefer to disable all icon fonts on the frontend for performance reasons, this dependency broke the lightbox controls.

To solve both problems, I created my own Elementor Static Gallery plugin — a lightweight, custom Elementor widget that loads images using plain HTML <img> elements, uses standard Unicode characters for the lightbox controls instead of icon fonts, and relies on minimal, efficient JavaScript solely to handle the lightbox’s open, close, and navigation behavior. The result is a gallery that loads instantly, respects native browser image handling (including lazy loading and responsive srcset attributes), and maintains a clean, responsive lightbox experience without any unnecessary scripts or external dependencies.

Place this PHP into a folder/file named elementor-static-gallery/elementor-static-gallery.php

				
					<?php
/**
 * Plugin Name: Elementor Static Gallery
 * Description: Lightweight Elementor gallery widget using native responsive images and a vanilla JS lightbox
 * Version: 1.0
 * Author: Tyler Ager
 */

if ( ! defined( 'ABSPATH' ) ) exit;

/*--------------------------------------------------------------
# Register Widget
--------------------------------------------------------------*/
add_action( 'elementor/widgets/register', function( $widgets_manager ) {

	class Elementor_Static_Gallery_Widget extends \Elementor\Widget_Base {

		public function get_name() { return 'static_gallery'; }
		public function get_title() { return __( 'Static Gallery', 'elementor' ); }
		public function get_icon() { return 'eicon-gallery-grid'; }
		public function get_categories() { return [ 'basic' ]; }

		protected function register_controls() {

			$this->start_controls_section( 'content_section', [
				'label' => __( 'Gallery', 'elementor' ),
				'tab'   => \Elementor\Controls_Manager::TAB_CONTENT,
			]);

			$this->add_control( 'gallery', [
				'label' => __( 'Add Images', 'elementor' ),
				'type'  => \Elementor\Controls_Manager::GALLERY,
			]);

			$this->add_responsive_control( 'columns', [
				'label'   => __( 'Columns', 'elementor' ),
				'type'    => \Elementor\Controls_Manager::NUMBER,
				'min'     => 1,
				'max'     => 6,
				'default' => 2,
			]);

			$this->add_control( 'gap', [
				'label'   => __( 'Gap (px)', 'elementor' ),
				'type'    => \Elementor\Controls_Manager::NUMBER,
				'default' => 10,
			]);

			$this->add_control( 'border_radius', [
				'label'   => __( 'Border Radius (px)', 'elementor' ),
				'type'    => \Elementor\Controls_Manager::NUMBER,
				'default' => 0,
			]);

			$this->add_control( 'aspect_ratio', [
				'label'   => __( 'Aspect Ratio', 'elementor' ),
				'type'    => \Elementor\Controls_Manager::SELECT,
				'default' => '1/1',
				'options' => [
					'1/1'  => 'Square 1:1',
					'4/3'  => 'Landscape 4:3',
					'3/2'  => 'Landscape 3:2',
					'16/9' => 'Widescreen 16:9',
					'2/3'  => 'Portrait 2:3',
					'3/4'  => 'Portrait 3:4',
					'9/16' => 'Tall 9:16',
					'auto' => 'Auto (use original)',
				],
			]);

			// Dynamically populate dropdown with all registered WP image sizes
			$image_sizes = get_intermediate_image_sizes();
			$options = array_combine( $image_sizes, $image_sizes );
			$options['full'] = __( 'Full Size', 'elementor' );

			$this->add_control( 'thumb_size', [
				'label'   => __( 'Thumbnail Size', 'elementor' ),
				'type'    => \Elementor\Controls_Manager::SELECT,
				'default' => 'medium',
				'options' => $options,
			]);

			$this->end_controls_section();
		}

		protected function render() {
			$s = $this->get_settings_for_display();
			$imgs = $s['gallery'];
			if ( empty( $imgs ) ) return;

			$cols       = max( 1, (int) $s['columns'] );
			$gap        = (int) $s['gap'];
			$radius     = (int) $s['border_radius'];
			$ratio      = esc_attr( $s['aspect_ratio'] );
			$thumb_size = ! empty( $s['thumb_size'] ) ? $s['thumb_size'] : 'medium';

			// Responsive column counts
			$cols_desktop = max( 1, (int) ( $s['columns'] ?? 2 ) );
			$cols_tablet  = max( 1, (int) ( $s['columns_tablet'] ?? 2 ) );
			$cols_mobile  = max( 1, (int) ( $s['columns_mobile'] ?? 1 ) );

			// Container width factors (relative to viewport)
			$container_factor_desktop = apply_filters( 'ta_static_gallery_container_factor_desktop', 0.5 ); // Gallery = 50% of viewport
			$container_factor_tablet  = apply_filters( 'ta_static_gallery_container_factor_tablet', 0.8 );  // ~80% on tablet
			$container_factor_mobile  = apply_filters( 'ta_static_gallery_container_factor_mobile', 1.0 );  // full width on mobile

			// Compute effective vw per image for each breakpoint
			$vw_desktop = round( (100 * $container_factor_desktop) / $cols_desktop, 2 );
			$vw_tablet  = round( (100 * $container_factor_tablet) / $cols_tablet, 2 );
			$vw_mobile  = round( (100 * $container_factor_mobile) / $cols_mobile, 2 );

			// Build sizes attribute dynamically
			$sizes_attr = sprintf(
				'(max-width: 600px) %svw, (max-width: 1024px) %svw, %svw',
				$vw_mobile,
				$vw_tablet,
				$vw_desktop
			);

			echo '<div class="static-gallery" style="--cols:' . $cols . ';--gap:' . $gap . 'px;--radius:' . $radius . 'px;--ratio:' . $ratio . ';">';

			$i = 0;
			foreach ( $imgs as $img ) {
				$i++;
				$id   = $img['id'];
				$full = wp_get_attachment_image_url( $id, 'full' );
				$alt  = esc_attr( get_post_meta( $id, '_wp_attachment_image_alt', true ) );

				$src    = wp_get_attachment_image_url( $id, $thumb_size );
				$srcset = wp_get_attachment_image_srcset( $id, $thumb_size );

				echo '<figure data-full="' . esc_url( $full ) . '" data-index="' . $i . '">';
				echo '<img decoding="async" src="' . esc_url( $src ) . '" srcset="' . esc_attr( $srcset ) . '" sizes="' . esc_attr( $sizes_attr ) . '" alt="' . $alt . '" loading="lazy">';
				echo '</figure>';
			}

			echo '
				<div class="lightbox" id="ta-lightbox">
					<span class="nav prev" role="button">&#10094;</span>
					<img decoding="async" class="lightbox-image" src="" alt="">
					<span class="nav next" role="button">&#10095;</span>
					<span class="close" role="button">✕</span>
				</div>
			</div>';
		}
	}

	$widgets_manager->register( new Elementor_Static_Gallery_Widget() );
});

/*--------------------------------------------------------------
# Conditional Asset Loading
--------------------------------------------------------------*/

// Load assets only when widget actually renders
add_action( 'elementor/frontend/after_render', function( $widget ) {
	if ( $widget->get_name() === 'static_gallery' ) {
		if ( ! defined( 'TA_STATIC_GALLERY_LOADED' ) ) {
			define( 'TA_STATIC_GALLERY_LOADED', true );
			ta_static_gallery_enqueue_assets();
		}
	}
}, 10, 1 );

// Always load in Elementor editor/preview
add_action( 'elementor/editor/after_enqueue_scripts', 'ta_static_gallery_enqueue_assets' );
add_action( 'elementor/preview/enqueue_styles', 'ta_static_gallery_enqueue_assets' );

/**
 * Enqueue gallery CSS and JS
 */
function ta_static_gallery_enqueue_assets() {
	$plugin_url = plugin_dir_url( __FILE__ );

	wp_enqueue_style(
		'static-gallery',
		$plugin_url . 'assets/static-gallery.css',
		[],
		'2.4.3'
	);

	wp_enqueue_script(
		'static-gallery',
		$plugin_url . 'assets/static-gallery.js',
		[],
		'2.4.3',
		true
	);
}

				
			

Place this JavaScript into a folder/file named elementor-static-gallery/assets/static-gallery.js

				
					(function () {
  const figures = document.querySelectorAll(".static-gallery figure");
  const lightbox = document.getElementById("ta-lightbox");
  if (!figures.length || !lightbox) return;

  // --- Ensure lightbox is appended directly to <body>
        <script data-pagespeed-no-defer data-two-no-delay type="text/javascript">

        </script>
         <script data-pagespeed-no-defer data-two-no-delay id="two_worker" type="javascript/worker">
                let two_font_actions = "not_load";
            let two_css_length=0;let two_connected_css_length=0;let two_uncritical_fonts=null;let two_uncritical_fonts_status=false;if(two_font_actions=="not_load"||two_font_actions=="exclude_uncritical_fonts"){two_uncritical_fonts_status=true;}
self.addEventListener("message",function(e){two_css_length=e.data.css.length;if(!e.data.critical_data.critical_css||!e.data.critical_data.critical_fonts){two_uncritical_fonts_status=false;}
if(e.data.font.length>0){two_fetch_inbg(e.data.font,"font");}
if(e.data.js.length>0){two_fetch_inbg(e.data.js,"js");}
if(e.data.excluded_js.length>0){two_fetch_inbg(e.data.excluded_js,"js",true);}
if(e.data.css.length>0){two_fetch_inbg(e.data.css,"css");}},false);function two_fetch_inbg(data,type,excluded_js=false){for(let i in data){if(typeof data[i].url!="undefined"){var modifiedScript=null;if(type==="js"&&typeof data[i].exclude_blob!="undefined"&&data[i].exclude_blob){modifiedScript={id:i,status:'ok',type:type,url:data[i].url,uid:data[i].uid};two_send_worker_data(modifiedScript);continue;}
fetch(data[i].url,{mode:'no-cors',redirect:'follow'}).then((r)=>{if(!r.ok||r.status!==200){throw Error(r.statusText);}
if(two_uncritical_fonts_status&&type=="css"){return(r.text());}else{return(r.blob());}}).then((content_)=>{let sheetURL="";if(two_uncritical_fonts_status&&type=="css"){sheetURL=two_create_blob(content_);}else{sheetURL=URL.createObjectURL(content_);}
modifiedScript=null;if(type=="css"){modifiedScript={id:i,type:type,status:'ok',media:data[i].media,url:sheetURL,uid:data[i].uid,original_url:data[i].url,two_uncritical_fonts:two_uncritical_fonts,};}else if(type=="js"){modifiedScript={id:i,status:'ok',type:type,url:sheetURL,uid:data[i].uid};}else if(type=="font"){modifiedScript={status:'ok',type:type,main_url:data[i].url,url:sheetURL,font_face:data[i].font_face};}
if(excluded_js){modifiedScript.excluded_from_delay=true;}
two_send_worker_data(modifiedScript);}).catch(function(error){console.log("error in fetching: "+error.toString()+", bypassing "+data[i].url);fetch(data[i].url,{redirect:'follow'}).then((r)=>{if(!r.ok||r.status!==200){throw Error(r.statusText);}
if(two_uncritical_fonts_status&&type=="css"){return(r.text());}else{return(r.blob());}}).then((content_)=>{let sheetURL="";if(two_uncritical_fonts_status&&type=="css"){sheetURL=two_create_blob(content_);}else{sheetURL=URL.createObjectURL(content_);}
var modifiedScript=null;if(type=="css"){modifiedScript={id:i,type:type,status:'ok',media:data[i].media,url:sheetURL,uid:data[i].uid,original_url:data[i].url,two_uncritical_fonts:two_uncritical_fonts,};}else if(type=="js"){modifiedScript={id:i,status:'ok',type:type,url:sheetURL,uid:data[i].uid};}else if(type=="font"){modifiedScript={status:'ok',type:type,main_url:data[i].url,url:sheetURL,font_face:data[i].font_face};}
if(excluded_js){modifiedScript.excluded_from_delay=true;}
two_send_worker_data(modifiedScript);}).catch(function(error){console.log("error in fetching no-cors: "+error.toString()+", bypassing "+data[i].url);try{console.log("error in fetching: "+error.toString()+", sending XMLHttpRequest"+data[i].url);let r=new XMLHttpRequest;if(two_uncritical_fonts_status&&type=="css"){r.responseType="text";}else{r.responseType="blob";}
r.onload=function(content_){let sheetURL="";if(two_uncritical_fonts_status&&type=="css"){sheetURL=two_create_blob(content_.target.response);}else{sheetURL=URL.createObjectURL(content_.target.response);}
if(r.status!==200){two_XMLHttpRequest_error(excluded_js,data[i],type,i);return;}
console.log("error in fetching: "+error.toString()+", XMLHttpRequest success "+data[i].url);let modifiedScript=null;if(type=="css"){modifiedScript={id:i,type:type,status:'ok',media:data[i].media,url:sheetURL,uid:data[i].uid,two_uncritical_fonts:two_uncritical_fonts,};}else if(type=="js"){modifiedScript={id:i,type:type,status:'ok',url:sheetURL,uid:data[i].uid};}else if(type=="font"){modifiedScript={type:type,status:'ok',main_url:data[i].url,url:sheetURL,font_face:data[i].font_face};}
if(excluded_js){modifiedScript.excluded_from_delay=true;}
two_send_worker_data(modifiedScript);};r.onerror=function(){two_XMLHttpRequest_error(excluded_js,data[i],type,i)};r.open("GET",data[i].url,true);r.send();}catch(e){console.log("error in fetching: "+e.toString()+", running fallback for "+data[i].url);var modifiedScript=null;if(type=="css"||type=="js"){modifiedScript={id:i,type:type,status:'error',url:data[i].url,uid:data[i].uid};}else if(type=="font"){modifiedScript={type:type,status:'error',url:data[i].url,font_face:data[i].font_face};}
if(excluded_js){modifiedScript.excluded_from_delay=true;}
two_send_worker_data(modifiedScript);}});});}}}
function two_XMLHttpRequest_error(excluded_js,data_i,type,i){console.log("error in fetching: XMLHttpRequest failed "+data_i.url);var modifiedScript=null;if(type=="css"||type=="js"){modifiedScript={id:i,type:type,status:'error',url:data_i.url,uid:data_i.uid};}else if(type=="font"){modifiedScript={type:type,status:'error',url:data_i.url,font_face:data_i.font_face};}
if(excluded_js){modifiedScript.excluded_from_delay=true;}
two_send_worker_data(modifiedScript);}
function two_create_blob(str){two_uncritical_fonts="";const regex=/@font-face\s*\{(?:[^{}])*\}/sig;str=str.replace(regex,function(e){if(e.includes("data:application")){return e;}
two_uncritical_fonts+=e;return"";});let blob_data=new Blob([str],{type:"text/css"});let sheetURL=URL.createObjectURL(blob_data);return sheetURL;}
function two_send_worker_data(data){if(data.type=="css"){two_connected_css_length++;data.length=two_css_length;data.connected_length=two_connected_css_length;}
self.postMessage(data)}
        </script>
        <script data-pagespeed-no-defer data-two-no-delay type="text/javascript">
                        let two_font_actions = "not_load";
                     two_worker_styles_list=[];two_worker_styles_count=0;var two_script_list=typeof two_worker_data_js==="undefined"?[]:two_worker_data_js.js;var two_excluded_js_list=typeof two_worker_data_excluded_js==="undefined"?[]:two_worker_data_excluded_js.js;var excluded_count=two_excluded_js_list.filter((el)=>{return!!el['url']}).length;var two_css_list=typeof two_worker_data_css==="undefined"?[]:two_worker_data_css.css;var two_fonts_list=typeof two_worker_data_font==="undefined"?[]:two_worker_data_font.font;var two_critical_data=typeof two_worker_data_critical_data==="undefined"?[]:two_worker_data_critical_data.critical_data;var wcode=new Blob([document.querySelector("#two_worker").textContent],{type:"text/javascript"});var two_worker=new Worker(window.URL.createObjectURL(wcode));var two_worker_data={"js":two_script_list,"excluded_js":two_excluded_js_list,"css":two_css_list,"font":two_fonts_list,critical_data:two_critical_data}
two_worker.postMessage(two_worker_data);two_worker.addEventListener("message",function(e){var data=e.data;if(data.type==="css"&&data.status==="ok"){if(data.two_uncritical_fonts&&two_font_actions=="exclude_uncritical_fonts"){let two_uncritical_fonts=data.two_uncritical_fonts;const two_font_tag=document.createElement("style");two_font_tag.innerHTML=two_uncritical_fonts;two_font_tag.className="two_uncritical_fonts";document.body.appendChild(two_font_tag);}
if(window.two_page_loaded){two_connect_style(data);}else{two_worker_styles_list.push(data);}}else if(data.type==="js"){if(data.status==="ok"){if(data.excluded_from_delay){two_excluded_js_list[data.id].old_url=two_excluded_js_list[data.id].url;two_excluded_js_list[data.id].url=data.url;two_excluded_js_list[data.id].success=true;excluded_count--;if(excluded_count===0){two_connect_script(0,two_excluded_js_list)}}else{two_script_list[data.id].old_url=two_script_list[data.id].url;two_script_list[data.id].url=data.url;two_script_list[data.id].success=true;}}}else if(data.type==="css"&&data.status==="error"){console.log("error in fetching, connecting style now")
two_connect_failed_style(data);}else if(data.type==="font"){two_connect_font(data);}});function UpdateQueryString(key,value,url){if(!url)url=window.location.href;var re=new RegExp("([?&])"+key+"=.*?(&|#|$)(.*)","gi"),hash;if(re.test(url)){if(typeof value!=="undefined"&&value!==null){return url.replace(re,"$1"+key+"="+value+"$2$3");}
else{hash=url.split("#");url=hash[0].replace(re,"$1$3").replace(/(&|\?)$/,"");if(typeof hash[1]!=="undefined"&&hash[1]!==null){url+="#"+hash[1];}
return url;}}
else{if(typeof value!=="undefined"&&value!==null){var separator=url.indexOf("?")!==-1?"&":"?";hash=url.split("#");url=hash[0]+separator+key+"="+value;if(typeof hash[1]!=="undefined"&&hash[1]!==null){url+="#"+hash[1];}
return url;}
else{return url;}}}
function two_connect_failed_style(data){var link=document.createElement("link");link.className="fallback_two_worker";link.rel="stylesheet";link.type="text/css";link.href=data.url;link.media="none";link.onload=function(){if(this.media==="none"){if(data.media){this.media=data.media;}else{this.media="all";}console.log(data.media);}if(data.connected_length==data.length&&typeof two_replace_backgrounds!="undefined"){two_replace_backgrounds();};two_styles_loaded()};document.getElementsByTagName("head")[0].appendChild(link);if(data.connected_length==data.length&&typeof two_replace_backgrounds!="undefined"){two_replace_backgrounds();}}
function two_connect_style(data,fixed_google_font=false){if(fixed_google_font===false&&typeof two_merge_google_fonts!=="undefined"&&data['original_url']&&data['original_url'].startsWith('https://fonts.googleapis.com/css')&&data['response']){data['response'].text().then(function(content){content=two_merge_google_fonts(content)
let blob=new Blob([content],{type:data['response'].type});data['url']=URL.createObjectURL(blob);two_connect_style(data,true);});return;}
var link=document.createElement("link");link.className="loaded_two_worker";link.rel="stylesheet";link.type="text/css";link.href=data.url;link.media=data.media;link.onload=function(){if(data.connected_length==data.length&&typeof two_replace_backgrounds!="undefined"){two_replace_backgrounds();};two_styles_loaded()};link.onerror=function(){two_styles_loaded()};document.getElementsByTagName("head")[0].appendChild(link);}
var two_event;function two_connect_script(i,scripts_list=null){if(i===0&&event){two_event=event;event.preventDefault();}
if(scripts_list===null){scripts_list=two_script_list;}
if(typeof scripts_list[i]!=="undefined"){let data_uid="[data-two_delay_id=\""+scripts_list[i].uid+"\"]";let current_script=document.querySelector(data_uid);let script=document.createElement("script");script.type="text/javascript";script.async=false;if(scripts_list[i].inline){var js_code=decodeURIComponent(atob(scripts_list[i].code));var blob=new Blob([js_code],{type:"text/javascript"});scripts_list[i].url=URL.createObjectURL(blob);}
if(current_script!=null&&typeof scripts_list[i].url!="undefined"){script.dataset.src=scripts_list[i].url;current_script.parentNode.insertBefore(script,current_script);current_script.getAttributeNames().map(function(name){let value=current_script.getAttribute(name);try{script.setAttribute(name,value);}catch(error){console.log(error);}});current_script.remove();script.classList.add("loaded_two_worker_js");if(typeof scripts_list[i].exclude_blob!="undefined"&&scripts_list[i].exclude_blob){script.dataset.blob_exclude="1";}}
i++;two_connect_script(i,scripts_list);}else{let elementor_frontend_js=null;document.querySelectorAll(".loaded_two_worker_js").forEach((elem)=>{let id=elem.getAttribute("id");if(id!='elementor-frontend-js'){two_load_delayed_js(elem);}else{elementor_frontend_js=elem;}});if(elementor_frontend_js!==null){two_load_delayed_js(elementor_frontend_js);}}}
function two_load_delayed_js(elem){let data_src=elem.dataset.src;if(elem.dataset.blob_exclude==="1"){delete elem.dataset.blob_exclude;delete elem.dataset.src;delete elem.dataset.two_delay_id;delete elem.dataset.two_delay_src;}
if(data_src){elem.setAttribute("src",data_src);}}
function two_connect_font(data){let font_face=data.font_face;if(font_face.indexOf("font-display")>=0){const regex=/font-display:[ ]*[a-z]*[A-Z]*;/g;while((m=regex.exec(font_face))!==null){if(m.index===regex.lastIndex){regex.lastIndex++;}
m.forEach((match,groupIndex)=>{console.log(match);font_face.replace(match,"font-display: swap;");});}}else{font_face=font_face.replace("}",";font-display: swap;}");}
if(typeof data.main_url!="undefined"){font_face=font_face.replace(data.main_url,data.url);}
var newStyle=document.createElement("style");newStyle.className="two_critical_font";newStyle.appendChild(document.createTextNode(font_face));document.head.appendChild(newStyle);}
let connect_stile_timeout=setInterval(function(){console.log(window.two_page_loaded);if(window.two_page_loaded){clearInterval(connect_stile_timeout);two_worker_styles_list.forEach(function(item,index){two_connect_style(item);});two_worker_styles_list=[];}},500);function two_styles_loaded(){if(two_css_list.length-++two_worker_styles_count==0){var critical_css=document.getElementById("two_critical_css");if(critical_css){critical_css.remove();}
onStylesLoadEvent=new Event("two_css_loaded");window.dispatchEvent(onStylesLoadEvent);}}
        </script> for correct z-index layering ---
  document.addEventListener("DOMContentLoaded", () => {
    if (lightbox.parentNode !== document.body) {
      document.body.appendChild(lightbox);
    }
  });

  let img = lightbox.querySelector(".lightbox-image");
  const prev = lightbox.querySelector(".prev");
  const next = lightbox.querySelector(".next");
  const close = lightbox.querySelector(".close");

  let index = 0;
  let isLoading = false;
  let lastURL = null;

  function open(i) {
    index = i;
    if (!lightbox.classList.contains("active")) {
      lightbox.classList.add("active");
      document.body.style.overflow = "hidden";
    }
    swapImage(index);
  }

  function closeBox() {
    lightbox.classList.remove("active");
    document.body.style.overflow = "";
    isLoading = false;
    lastURL = null;
    if (img) img.removeAttribute("src");
  }

  function showNext(dir) {
    if (isLoading) return;
    index = (index + dir + figures.length) % figures.length;
    swapImage(index);
  }

  // --- SAFARI-SAFE IMAGE SWAP ---
  function swapImage(i) {
    const f = figures[i];
    const full = f.dataset.full;
    const alt = f.querySelector("img").alt;

    if (isLoading && full === lastURL) return;
    isLoading = true;
    lastURL = full;

    prev.style.pointerEvents = "none";
    next.style.pointerEvents = "none";
    close.style.pointerEvents = "none";

    const newImg = document.createElement("img");
    newImg.className = "lightbox-image";
    newImg.alt = alt;
    newImg.style.opacity = "0";
    newImg.decoding = "async";
    newImg.loading = "eager";
    newImg.setAttribute("fetchpriority", "high");
    img.after(newImg);

    const preload = new Image();
    preload.decoding = "async";

    let finalized = false;

    function finalizeSuccess() {
      if (finalized) return;
      finalized = true;
      img.remove();
      img = newImg;
      img.style.opacity = "";
      isLoading = false;
      prev.style.pointerEvents = "";
      next.style.pointerEvents = "";
      close.style.pointerEvents = "";
    }

    function finalizeFailure() {
      if (finalized) return;
      finalized = true;
      const busted = full + (full.includes("?") ? "&" : "?") + "v=" + Date.now();
      newImg.src = busted;
      newImg.onload = finalizeSuccess;
      newImg.onerror = () => {
        img.style.opacity = "";
        isLoading = false;
      };
    }

    preload.onload = function () {
      requestAnimationFrame(() =>
        requestAnimationFrame(() => {
          newImg.src = full;
          if (newImg.complete) finalizeSuccess();
          else newImg.onload = finalizeSuccess;
        })
      );
    };

    preload.onerror = finalizeFailure;
    preload.src = full;

    // Fallback if Safari silently drops the request
    setTimeout(() => {
      if (!finalized && !newImg.src) {
        newImg.src = full;
        if (newImg.complete) finalizeSuccess();
      }
    }, 500);
  }

  // --- EVENT BINDINGS ---
  figures.forEach((f, i) => f.addEventListener("click", () => open(i)));
  prev.addEventListener("click", () => showNext(-1));
  next.addEventListener("click", () => showNext(1));
  close.addEventListener("click", closeBox);
  lightbox.addEventListener("click", (e) => {
    if (e.target === lightbox) closeBox();
  });

  document.addEventListener("keydown", (e) => {
    if (!lightbox.classList.contains("active")) return;
    if (e.key === "Escape") closeBox();
    if (e.key === "ArrowLeft") showNext(-1);
    if (e.key === "ArrowRight") showNext(1);
  });

  // --- BFCache / history restore fix ---
  window.addEventListener("pageshow", (e) => {
    if (e.persisted) {
      const lb = document.getElementById("ta-lightbox");
      if (lb) lb.classList.remove("active");
      document.body.style.overflow = "";
    }
  });
})();

				
			

Place this CSS into a folder/file named elementor-static-gallery/assets/static-gallery.css

				
					.static-gallery {
	display: grid;
	grid-template-columns: repeat(var(--cols), 1fr);
	gap: var(--gap);
}
.static-gallery figure {
	margin: 0;
	overflow: hidden;
	border-radius: var(--radius);
}
.static-gallery figure:not([style*="--ratio:auto"]) {
	aspect-ratio: var(--ratio);
}
.static-gallery img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
	cursor: pointer;
	transition: transform .3s;
}
/* No zoom on hover */
.static-gallery img:hover { transform: none; }

/* Lightbox */
.lightbox {
	position: fixed;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	width: 100vw;
	height: 100vh;
	display: none;
	align-items: center;
	justify-content: center;
	z-index: 9999;
	box-sizing: border-box;
	padding: 2rem;
	background: rgba(0,0,0,0.9);
}
.lightbox.active { display: flex; }

.lightbox img.lightbox-image {
	display: block;
	width: auto;
	height: auto;
	max-width: 90vw !important;
	max-height: 90vh !important;
	object-fit: contain !important;
	margin: auto;
	box-shadow: 0 0 25px rgba(0,0,0,.6);
}

/* Controls */
.lightbox .nav,
.lightbox .close {
	position: absolute;
	color: #fff;
	font-size: 48px;
	cursor: pointer;
	user-select: none;
	opacity: 0.85;
	transition: opacity .2s, transform .2s;
	z-index: 5;
	line-height: 1;
	padding: 20px;

	/* Subtle outline for visibility on light images */
	-webkit-text-stroke: 0.5px rgba(0,0,0,0.5);
	text-shadow: 0.5px 0.5px 1px rgba(0,0,0,0.3), -0.5px -0.5px 1px rgba(0,0,0,0.3);
}
.lightbox .nav:hover,
.lightbox .close:hover {
	opacity: 1;
	transform: scale(1.1);
}
.lightbox .nav.prev { left: 10px; }
.lightbox .nav.next { right: 10px; }
.lightbox .close { top: 15px; right: 25px; font-size: 40px; }

@media (max-width: 767px) {
	.lightbox .nav { font-size: 40px; padding: 30px 20px; }
	.lightbox .nav.prev { left: 5px; }
	.lightbox .nav.next { right: 5px; }
}

				
			

If this code helped you, or you have any questions on implementation, don’t hesitate to reach out.