Elementor Simple 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 Simple 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-simple-gallery/elementor-simple-gallery.php

				
					<?php
/**
 * Plugin Name: Elementor Simple Gallery
 * Description: Lightweight Elementor gallery widget using native responsive images and a vanilla JS lightbox (no JS image loading).
 * Version: 1.0
 * Author: Tyler Ager
 */

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

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

	class Elementor_Simple_Gallery_Widget extends \Elementor\Widget_Base {

		public function get_name() { return 'simple_gallery'; }
		public function get_title() { return __( 'Simple 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)',
				],
			]);

			$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();
		}

		public function render_plain_content() {
			$this->render();
		}

		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';

			$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_factor_desktop = apply_filters( 'ta_simple_gallery_container_factor_desktop', 0.5 );
			$container_factor_tablet  = apply_filters( 'ta_simple_gallery_container_factor_tablet', 0.8 );
			$container_factor_mobile  = apply_filters( 'ta_simple_gallery_container_factor_mobile', 1.0 );

			$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 );

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

			echo '<div class="simple-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_Simple_Gallery_Widget() );
});

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

add_action( 'elementor/frontend/after_render', function( $widget ) {
	if ( $widget->get_name() === 'simple_gallery' ) {
		if ( ! defined( 'TA_SIMPLE_GALLERY_LOADED' ) ) {
			define( 'TA_SIMPLE_GALLERY_LOADED', true );
			ta_simple_gallery_enqueue_assets();
		}
	}
}, 10, 1 );

add_action( 'elementor/editor/after_enqueue_scripts', 'ta_simple_gallery_enqueue_assets' );
add_action( 'elementor/preview/enqueue_styles', 'ta_simple_gallery_enqueue_assets' );

function ta_simple_gallery_enqueue_assets() {
	$plugin_url = plugin_dir_url( __FILE__ );

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

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

				
			

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

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

  // Ensure lightbox is appended directly to body for correct z-index layering
  function moveLightboxToBody() {
    if (lightbox.parentNode !== document.body) {
      document.body.appendChild(lightbox);
    }
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", moveLightboxToBody);
  } else {
    moveLightboxToBody();
  }

  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 with robust fallbacks
  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);
  }

  // Events
  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-simple-gallery/assets/simple-gallery.css

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

	/* outline over 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.