Created
December 11, 2025 11:51
-
-
Save Qubadi/ca5073bc3392265bf8a2944b80c5b391 to your computer and use it in GitHub Desktop.
Modern responsive Bento grids in Elementor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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