
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
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 '';
$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 '';
echo '
';
echo ' ';
}
echo '
❮
❯
✕
';
}
}
$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
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; }
}