Skip to content

Instantly share code, notes, and snippets.

@Qubadi
Created December 11, 2025 11:51
Show Gist options
  • Select an option

  • Save Qubadi/ca5073bc3392265bf8a2944b80c5b391 to your computer and use it in GitHub Desktop.

Select an option

Save Qubadi/ca5073bc3392265bf8a2944b80c5b391 to your computer and use it in GitHub Desktop.
Modern responsive Bento grids in Elementor
Copy the following PHP and create a PHP snippet using your snippet plugins.
Paste the code into the plugin and save it.
_____________________________________________
/**
* Widget Name: Modern Bento Gallery
* Description: Airbnb-style grid (Desktop) + Native Smooth Slider (Mobile).
* Fixes:
* 1. Counter logic now correctly calculates Total - Limit (e.g., 6 - 3 = +3).
* 2. Lightbox now includes hidden images by using hidden <a> tags instead of <div>.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
add_action( 'elementor/widgets/register', function( $widgets_manager ) {
class Elementor_Modern_Bento_Gallery extends \Elementor\Widget_Base {
public function get_name() {
return 'modern_bento_gallery';
}
public function get_title() {
return esc_html__( 'Modern Bento Gallery', 'elementor-custom' );
}
public function get_icon() {
return 'eicon-gallery-grid';
}
public function get_categories() {
return [ 'general' ];
}
public function get_script_depends() {
return [ 'elementor-frontend' ];
}
protected function register_controls() {
// ================= CONTENT TAB =================
$this->start_controls_section(
'section_content',
[
'label' => esc_html__( 'Gallery Content', 'elementor-custom' ),
]
);
$this->add_control(
'gallery_images',
[
'label' => esc_html__( 'Images', 'elementor-custom' ),
'type' => \Elementor\Controls_Manager::GALLERY,
'dynamic' => [ 'active' => true ],
'default' => [],
'description' => 'Select images or use Dynamic Tags (ACF/JetEngine).',
]
);
$this->add_control(
'visible_count',
[
'label' => esc_html__( 'Desktop Layout (Count)', 'elementor-custom' ),
'type' => \Elementor\Controls_Manager::SLIDER,
'range' => [
'px' => [ 'min' => 3, 'max' => 5, 'step' => 1 ],
],
'default' => [ 'size' => 5 ],
'description' => 'Min 3, Max 5. Controls the grid layout.',
]
);
$this->add_control(
'overlay_label',
[
'label' => esc_html__( 'Overlay Text', 'elementor-custom' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => 'Show all photos',
]
);
$this->end_controls_section();
// ================= STYLE TAB: LAYOUT =================
$this->start_controls_section(
'section_style_layout',
[
'label' => esc_html__( 'Layout & Grid', 'elementor-custom' ),
'tab' => \Elementor\Controls_Manager::TAB_STYLE,
]
);
$this->add_responsive_control(
'gallery_height',
[
'label' => esc_html__( 'Desktop Height', 'elementor-custom' ),
'type' => \Elementor\Controls_Manager::SLIDER,
'size_units' => [ 'px', 'vh' ],
'range' => [ 'px' => [ 'min' => 200, 'max' => 1000 ] ],
'default' => [ 'unit' => 'px', 'size' => 450 ],
'selectors' => [
'{{WRAPPER}} .bento-grid-wrapper' => 'height: {{SIZE}}{{UNIT}};',
],
]
);
$this->add_responsive_control(
'grid_gap',
[
'label' => esc_html__( 'Gap', 'elementor-custom' ),
'type' => \Elementor\Controls_Manager::SLIDER,
'range' => [ 'px' => [ 'min' => 0, 'max' => 50 ] ],
'default' => [ 'unit' => 'px', 'size' => 8 ],
'selectors' => [
'{{WRAPPER}} .bento-grid-wrapper' => 'gap: {{SIZE}}{{UNIT}};',
'{{WRAPPER}} .bento-mobile-wrapper' => 'gap: {{SIZE}}{{UNIT}};',
'{{WRAPPER}} .bento-mobile-slider' => 'gap: {{SIZE}}{{UNIT}}; margin-top: {{SIZE}}{{UNIT}};',
],
]
);
$this->add_control(
'border_radius',
[
'label' => esc_html__( 'Border Radius', 'elementor-custom' ),
'type' => \Elementor\Controls_Manager::DIMENSIONS,
'size_units' => [ 'px', '%' ],
'selectors' => [
'{{WRAPPER}} .bento-item' => 'border-radius: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
],
]
);
$this->end_controls_section();
// ================= STYLE TAB: MOBILE / TABLET =================
$this->start_controls_section(
'section_style_mobile',
[
'label' => esc_html__( 'Mobile & Tablet', 'elementor-custom' ),
'tab' => \Elementor\Controls_Manager::TAB_STYLE,
]
);
$this->add_responsive_control(
'mobile_padding',
[
'label' => esc_html__( 'Container Padding', 'elementor-custom' ),
'type' => \Elementor\Controls_Manager::DIMENSIONS,
'size_units' => [ 'px', '%', 'em' ],
'default' => [
'top' => 0, 'right' => 20, 'bottom' => 0, 'left' => 20,
'unit' => 'px', 'isLinked' => false,
],
'selectors' => [
'{{WRAPPER}} .bento-mobile-wrapper' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
],
]
);
$this->add_responsive_control(
'mobile_main_height',
[
'label' => esc_html__( 'Main Image Height', 'elementor-custom' ),
'type' => \Elementor\Controls_Manager::SLIDER,
'size_units' => [ 'px' ],
'range' => [ 'px' => [ 'min' => 100, 'max' => 600 ] ],
'default' => [ 'unit' => 'px', 'size' => 250 ],
'selectors' => [
'{{WRAPPER}} .bento-main-mobile' => 'height: {{SIZE}}{{UNIT}};',
],
]
);
$this->add_responsive_control(
'mobile_thumb_width',
[
'label' => esc_html__( 'Thumbnail Size', 'elementor-custom' ),
'type' => \Elementor\Controls_Manager::SLIDER,
'size_units' => [ 'px', '%' ],
'range' => [
'%' => [ 'min' => 20, 'max' => 90 ],
'px' => [ 'min' => 80, 'max' => 400 ],
],
'default' => [ 'unit' => '%', 'size' => 40 ],
'selectors' => [
'{{WRAPPER}} .bento-sub-item' => 'width: {{SIZE}}{{UNIT}}; flex: 0 0 {{SIZE}}{{UNIT}};',
],
]
);
$this->end_controls_section();
// ================= STYLE TAB: OVERLAY =================
$this->start_controls_section(
'section_style_overlay',
[
'label' => esc_html__( 'Overlay & Text', 'elementor-custom' ),
'tab' => \Elementor\Controls_Manager::TAB_STYLE,
]
);
$this->add_control(
'overlay_bg_color',
[
'label' => esc_html__( 'Overlay Background', 'elementor-custom' ),
'type' => \Elementor\Controls_Manager::COLOR,
'default' => 'rgba(0, 0, 0, 0.5)',
'selectors' => [
'{{WRAPPER}} .bento-overlay' => 'background-color: {{VALUE}};',
],
]
);
$this->add_control( 'heading_count', [ 'type' => \Elementor\Controls_Manager::HEADING, 'label' => 'Counter (+X)' ] );
$this->add_control(
'count_color',
[
'label' => 'Color',
'type' => \Elementor\Controls_Manager::COLOR,
'default' => '#ffffff',
'selectors' => [ '{{WRAPPER}} .bento-count' => 'color: {{VALUE}};' ],
]
);
$this->add_group_control(
\Elementor\Group_Control_Typography::get_type(),
[
'name' => 'count_typography',
'selector' => '{{WRAPPER}} .bento-count',
'fields_options' => [
'typography' => [ 'default' => 'custom' ],
'font_size' => [ 'default' => [ 'size' => 32, 'unit' => 'px' ] ],
'font_weight' => [ 'default' => 600 ],
],
]
);
$this->add_control( 'heading_label', [ 'type' => \Elementor\Controls_Manager::HEADING, 'label' => 'Label Text', 'separator' => 'before' ] );
$this->add_control(
'label_color',
[
'label' => 'Color',
'type' => \Elementor\Controls_Manager::COLOR,
'default' => '#ffffff',
'selectors' => [ '{{WRAPPER}} .bento-label' => 'color: {{VALUE}};' ],
]
);
$this->add_group_control(
\Elementor\Group_Control_Typography::get_type(),
[
'name' => 'label_typography',
'selector' => '{{WRAPPER}} .bento-label',
]
);
$this->end_controls_section();
}
private function get_images( $settings ) {
$raw = $settings['gallery_images'];
$normalized = [];
if ( ! is_array( $raw ) || empty( $raw ) ) return [];
foreach ( $raw as $item ) {
if ( is_array( $item ) && isset( $item['url'] ) && ! empty( $item['url'] ) ) {
$normalized[] = $item;
continue;
}
$id = 0;
if ( is_numeric( $item ) ) $id = $item;
elseif ( is_array( $item ) && isset( $item['id'] ) ) $id = $item['id'];
if ( $id > 0 ) {
$url_data = wp_get_attachment_image_src( $id, 'full' );
if ( $url_data ) {
$normalized[] = [
'id' => $id,
'url' => $url_data[0],
'title' => get_the_title( $id ),
];
}
}
}
return $normalized;
}
protected function render() {
$settings = $this->get_settings_for_display();
$images = $this->get_images( $settings );
if ( empty( $images ) ) return;
// FIX: Distinct IDs for Desktop vs Mobile to prevent double-counting in Lightbox
$base_id = $this->get_id();
$desktop_id = 'bento_d_' . $base_id;
$mobile_id = 'bento_m_' . $base_id;
$slider_id = 'bento_slider_' . $base_id;
// Logic: Clamp between 3 and 5
$raw_limit = isset($settings['visible_count']['size']) ? (int)$settings['visible_count']['size'] : 5;
$limit = max( 3, min( 5, $raw_limit ) );
$total = count( $images );
// FIX 1: Correct math (Total - Limit)
// Example: 6 images, 3 visible. 6 - 3 = 3 (Shows +3).
$display_count = $total - $limit;
?>
<style>
.bento-item {
display: block; position: relative;
background-size: cover; background-position: center; background-repeat: no-repeat;
cursor: pointer; overflow: hidden; text-decoration: none;
-webkit-user-drag: none; user-select: none;
}
.bento-item::after {
content: ''; position: absolute; top:0; left:0; width:100%; height:100%;
background: rgba(0,0,0,0); transition: background 0.3s;
}
.bento-item:hover::after { background: rgba(0,0,0,0.1); }
.bento-overlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
display: flex; flex-direction: column; align-items: center; justify-content: center;
text-align: center; z-index: 5; transition: background-color 0.3s ease;
}
.bento-count, .bento-label { pointer-events: none; user-select: none; line-height: 1.2; }
.bento-count { display: block; margin-bottom: 5px; }
/* DESKTOP GRID */
@media (min-width: 1025px) {
.bento-mobile-wrapper { display: none; }
.bento-grid-wrapper { display: grid; width: 100%; box-sizing: border-box; }
<?php if ( $limit == 3 ) : ?>
.bento-grid-wrapper { grid-template-columns: 1.5fr 1fr; grid-template-rows: 1fr 1fr; }
.bento-d-0 { grid-column: 1 / 2; grid-row: 1 / 3; }
.bento-d-1 { grid-column: 2 / 3; grid-row: 1 / 2; }
.bento-d-2 { grid-column: 2 / 3; grid-row: 2 / 3; }
<?php elseif ( $limit == 4 ) : ?>
.bento-grid-wrapper { grid-template-columns: 1.5fr 1fr; grid-template-rows: 1fr 1fr 1fr; }
.bento-d-0 { grid-column: 1 / 2; grid-row: 1 / 4; }
.bento-d-1 { grid-column: 2 / 3; grid-row: 1 / 2; }
.bento-d-2 { grid-column: 2 / 3; grid-row: 2 / 3; }
.bento-d-3 { grid-column: 2 / 3; grid-row: 3 / 4; }
<?php else : ?>
.bento-grid-wrapper { grid-template-columns: 2fr 1fr 1fr; grid-template-rows: 1fr 1fr; }
.bento-d-0 { grid-column: 1 / 2; grid-row: 1 / 3; }
.bento-d-1 { grid-column: 2 / 3; grid-row: 1 / 2; }
.bento-d-2 { grid-column: 3 / 4; grid-row: 1 / 2; }
.bento-d-3 { grid-column: 2 / 3; grid-row: 2 / 3; }
.bento-d-4 { grid-column: 3 / 4; grid-row: 2 / 3; }
<?php endif; ?>
}
/* MOBILE FLEX */
@media (max-width: 1024px) {
.bento-grid-wrapper { display: none; }
.bento-mobile-wrapper { display: flex; flex-direction: column; width: 100%; box-sizing: border-box; }
.bento-main-mobile { width: 100%; }
.bento-mobile-slider {
display: flex; overflow-x: auto; scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch; padding-bottom: 5px; scrollbar-width: none;
cursor: grab;
}
.bento-mobile-slider::-webkit-scrollbar { display: none; }
.bento-mobile-slider.active { cursor: grabbing; }
.bento-sub-item {
aspect-ratio: 4/3; scroll-snap-align: start; margin-right: 10px; flex-shrink: 0;
}
.bento-sub-item:last-child { margin-right: 0; }
.bento-overlay .bento-count { font-size: 20px !important; }
.bento-overlay .bento-label { font-size: 11px !important; display: none; }
@media (min-width: 600px) { .bento-overlay .bento-label { display: block; } }
}
</style>
<!-- DESKTOP CONTAINER -->
<div class="bento-grid-wrapper">
<?php
// Loop Visible Images for Desktop
for ( $i = 0; $i < $limit; $i++ ) :
if ( ! isset( $images[$i] ) ) break;
$url = esc_url($images[$i]['url']);
$lb_attr = 'data-elementor-open-lightbox="yes" data-elementor-lightbox-slideshow="' . esc_attr($desktop_id) . '"';
$is_last = ( $i === $limit - 1 );
$has_more = ( $total > $limit );
?>
<a class="bento-item bento-d-<?php echo $i; ?>"
href="<?php echo $url; ?>" style="background-image: url('<?php echo $url; ?>');" <?php echo $lb_attr; ?>>
<?php if ( $is_last && $has_more ) : ?>
<div class="bento-overlay">
<span class="bento-count">+<?php echo esc_html( $display_count ); ?></span>
<span class="bento-label"><?php echo esc_html( $settings['overlay_label'] ); ?></span>
</div>
<?php endif; ?>
</a>
<?php endfor; ?>
<?php
// FIX 2: Use hidden <a> tags instead of <div> so Elementor Lightbox detects them
for ( $k = $limit; $k < $total; $k++ ) : ?>
<a style="display:none;"
href="<?php echo esc_url($images[$k]['url']); ?>"
data-elementor-open-lightbox="yes"
data-elementor-lightbox-slideshow="<?php echo esc_attr($desktop_id); ?>"></a>
<?php endfor; ?>
</div>
<!-- MOBILE CONTAINER -->
<div class="bento-mobile-wrapper">
<?php
// Mobile Main Image (Index 0)
if ( isset($images[0]) ) :
$m_url = esc_url($images[0]['url']);
$lb_attr_m = 'data-elementor-open-lightbox="yes" data-elementor-lightbox-slideshow="' . esc_attr($mobile_id) . '"';
?>
<a class="bento-item bento-main-mobile"
href="<?php echo $m_url; ?>" style="background-image: url('<?php echo $m_url; ?>');" <?php echo $lb_attr_m; ?>></a>
<?php endif; ?>
<div class="bento-mobile-slider" id="<?php echo esc_attr( $slider_id ); ?>">
<?php
// Mobile Slider Images (Index 1 to Limit)
for ( $j = 1; $j < $limit; $j++ ) :
if ( ! isset( $images[$j] ) ) break;
$s_url = esc_url($images[$j]['url']);
$is_last_slide = ( $j === $limit - 1 );
$has_more_mobile = ( $total > $limit );
$lb_attr_m = 'data-elementor-open-lightbox="yes" data-elementor-lightbox-slideshow="' . esc_attr($mobile_id) . '"';
?>
<a class="bento-item bento-sub-item"
href="<?php echo $s_url; ?>" style="background-image: url('<?php echo $s_url; ?>');" <?php echo $lb_attr_m; ?>>
<?php if ( $is_last_slide && $has_more_mobile ) : ?>
<div class="bento-overlay">
<span class="bento-count">+<?php echo esc_html( $display_count ); ?></span>
<span class="bento-label"><?php echo esc_html( $settings['overlay_label'] ); ?></span>
</div>
<?php endif; ?>
</a>
<?php endfor; ?>
</div>
<?php
// FIX 2: Use hidden <a> tags for Mobile as well
for ( $k = $limit; $k < $total; $k++ ) : ?>
<a style="display:none;"
href="<?php echo esc_url($images[$k]['url']); ?>"
data-elementor-open-lightbox="yes"
data-elementor-lightbox-slideshow="<?php echo esc_attr($mobile_id); ?>"></a>
<?php endfor; ?>
</div>
<!-- MOUSE DRAG SCRIPT (Desktop Only) -->
<script>
(function(){
const slider = document.getElementById('<?php echo $slider_id; ?>');
if(!slider) return;
let isDown = false;
let startX;
let scrollLeft;
let isDragging = false;
const isTouch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
if ( !isTouch ) {
slider.addEventListener('mousedown', (e) => {
isDown = true;
isDragging = false;
slider.classList.add('active');
startX = e.pageX - slider.offsetLeft;
scrollLeft = slider.scrollLeft;
});
slider.addEventListener('mouseleave', () => { isDown = false; slider.classList.remove('active'); });
slider.addEventListener('mouseup', () => {
isDown = false;
slider.classList.remove('active');
setTimeout(() => { isDragging = false; }, 50);
});
slider.addEventListener('mousemove', (e) => {
if(!isDown) return;
e.preventDefault();
const x = e.pageX - slider.offsetLeft;
const walk = (x - startX) * 2;
if(Math.abs(x - startX) > 5) isDragging = true;
slider.scrollLeft = scrollLeft - walk;
});
}
const links = slider.querySelectorAll('a');
links.forEach(link => {
link.addEventListener('click', (e) => {
if(isDragging) { e.preventDefault(); e.stopImmediatePropagation(); return false; }
});
});
})();
</script>
<?php
}
}
$widgets_manager->register( new Elementor_Modern_Bento_Gallery() );
}, 99 );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment