Building a Custom Nested Widget in Elementor (Step-by-Step)

Samuel HadsallSamuel Hadsall

A practical guide to creating a parent “container” widget that can hold draggable child items directly inside the Elementor editor—without relying on Elementor Pro.

What you’ll build

A nested “Slider” style widget where the parent widget contains multiple child “Slide” items. Editors can add, reorder, and edit slides in-place inside Elementor’s panel, just like built-in nested elements (Tabs/Accordion/etc.).

Prerequisites

  • WordPress + Elementor installed (nested elements require relatively recent Elementor versions).
  • Comfort with Elementor widget development (PHP) and a small editor-only JavaScript file.
  • A JS build step (or a simple enqueue of a plain JS file) that runs only in the Elementor editor.

Why nested widgets are different

A normal widget (Widget_Base) can render markup, but Elementor’s editor won’t treat it as a container that can host draggable children. For that, the parent widget must extend Elementor’s nested base and you must register a nested element type on the editor side.

High-level architecture

You’ll implement two widgets and one editor script:

  • Parent widget: a container (e.g., Slider) that editors drop into a page.
  • Child widget: a repeatable item (e.g., Slide) that lives inside the parent.
  • Editor JS: registers the parent as a nested container type and applies editor-only classes/behavior to child views (so they look/behave like slides).

Step 1: Create the child widget (the nested item)

The child widget is usually a regular widget (Widget_Base). It defines the controls for a single item (title, text, image, link, etc.) and outputs its markup.

  • Key points:
  • Keep the child widget self-contained: it should not assume how many siblings exist.
  • Avoid heavy JS in the child unless needed; let the parent’s frontend JS handle the slider behavior.

Example child widget skeleton (PHP):

<?php
 
use Elementor\Widget_Base;
 
use Elementor\Controls_Manager;
 
class SR_Slide_Item extends Widget_Base {
 
  public function get_name() { return 'sr-slide-item'; }
 
  public function get_title() { return __( 'Slide Item', 'sr' ); }
 
  public function get_icon() { return 'eicon-slider-device'; }
 
  public function get_categories() { return [ 'general' ]; }
 
  protected function register_controls() {
 
    $this->start_controls_section('content', [
 
      'label' => __('Content', 'sr'),
 
      'tab'   => Controls_Manager::TAB_CONTENT,
 
    ]);
 
    $this->add_control('heading', [
 
      'label' => __('Heading', 'sr'),
 
      'type'  => Controls_Manager::TEXT,
 
      'default' => __('Slide heading', 'sr'),
 
    ]);
 
    $this->add_control('body', [
 
      'label' => __('Body', 'sr'),
 
      'type'  => Controls_Manager::TEXTAREA,
 
      'default' => __('Slide copy…', 'sr'),
 
    ]);
 
    $this->end_controls_section();
 
  }
 
  protected function render() {
 
    $s = $this->get_settings_for_display();
 
    ?>
 
      <div class="sr-slide">
 
        <h3 class="sr-slide__title"><?php echo esc_html($s['heading']); ?></h3>
 
        <div class="sr-slide__body"><?php echo esc_html($s['body']); ?></div>
 
      </div>
 
    <?php
 
  }
 
}

Step 2: Create the parent widget (the nested container)

The parent widget is what makes this “nested.” It must extend Elementor’s nested base so Elementor knows it can host child widgets.

  • The parent widget typically defines:
  • Which child widget types are allowed inside it.
  • What default children should be created when the widget is first dropped in.
  • A render wrapper that outputs the container and prints children.

Example parent widget skeleton (PHP):

<?php
 
use Elementor\Controls_Manager;
 
use Elementor\Modules\NestedElements\Widget_Nested_Base;
 
class SR_Nested_Slider extends Widget_Nested_Base {
 
  public function get_name() { return 'sr-nested-slider'; } // must match editor JS getType()
 
  public function get_title() { return __( 'Nested Slider', 'sr' ); }
 
  public function get_icon() { return 'eicon-slider-device'; }
 
  public function get_categories() { return [ 'general' ]; }
 
  /**
 
   * Which widgets can be used as children.
 
   */
 
  public function get_nested_element_type() {
 
    return 'sr-slide-item';
 
  }
 
  /**
 
   * Optional: seed the widget with 2 default slides.
 
   */
 
  protected function get_default_children_elements() {
 
    return [
 
      [
 
        'elType'    => 'widget',
 
        'widgetType'=> 'sr-slide-item',
 
        'settings'  => [ 'heading' => 'Slide 1', 'body' => 'First slide copy…' ],
 
      ],
 
      [
 
        'elType'    => 'widget',
 
        'widgetType'=> 'sr-slide-item',
 
        'settings'  => [ 'heading' => 'Slide 2', 'body' => 'Second slide copy…' ],
 
      ],
 
    ];
 
  }
 
  protected function register_controls() {
 
    $this->start_controls_section('content', [
 
      'label' => __('Slider', 'sr'),
 
      'tab'   => Controls_Manager::TAB_CONTENT,
 
    ]);
 
    $this->add_control('autoplay', [
 
      'label' => __('Autoplay', 'sr'),
 
      'type'  => Controls_Manager::SWITCHER,
 
      'default' => 'yes',
 
    ]);
 
    $this->end_controls_section();
 
  }
 
  protected function render() {
 
    ?>
 
      <div class="sr-slider swiper">
 
        <div class="swiper-wrapper">
 
          <?php $this->print_children(); ?>
 
        </div>
 
        <div class="swiper-pagination"></div>
 
      </div>
 
    <?php
 
  }
 
}

Note: method names can vary slightly across Elementor versions. The important concept is that the parent extends Widget_Nested_Base and outputs a wrapper that prints its children.

Step 3: Register both widgets

Register both widgets in your plugin’s Elementor integration. The child must be registered like any other widget; the parent must also be registered normally—nested behavior comes from the base class + editor JS.

add_action('elementor/widgets/register', function($widgets_manager){
 
  require_once __DIR__ . '/widgets/class-sr-slide-item.php';
 
  require_once __DIR__ . '/widgets/class-sr-nested-slider.php';
 
  $widgets_manager->register( new \SR_Slide_Item() );
 
  $widgets_manager->register( new \SR_Nested_Slider() );
 
});

Step 4: Add the editor JavaScript (the missing piece)

This is the part that trips most people up. Without editor-side registration, Elementor will render the parent widget, but it won’t behave as a nested container in the editor. The script below listens for Elementor’s nested system to load, then registers your nested element type and attaches a custom NestedView.

This is the editor script that made everything “click” for you (keep it editor-only):

(function () {
 
  if (!window.elementor || !window.elementorCommon || !window.$e) return;
 
  elementorCommon.elements.$window.on('elementor/nested-element-type-loaded', function () {
 
    // 1) Grab Elementor’s NestedView base
 
    const NestedView = $e.components.get('nested-elements').exports.NestedView;
 
    // 2) Your view for nested children inside the slider
 
    class SRCarouselView extends NestedView {
 
      // Optional: keep a 1-based index around (helps with ARIA/labels if needed)
 
      filter(child, index) {
 
        child.attributes.dataIndex = index + 1;
 
        return true;
 
      }
 
      onAddChild(childView) {
 
        // Make each nested child behave like a swiper slide in the editor
 
        childView.$el.addClass('sp-e-item sp-e-wrp swiper-slide');
 
      }
 
    }
 
    // 3) The nested element type descriptor for your widget
 
    class SRNestedSlider extends elementor.modules.elements.types.NestedElementBase {
 
      getType() { return 'sr-nested-slider'; }      // MUST match PHP get_name()
 
      getView() { return SRCarouselView; }
 
    }
 
    // 4) Register with the elements manager
 
    elementor.elementsManager.registerElementType(new SRNestedSlider());
 
  });
 
})();
 
Enqueue it only in the editor:
 
add_action('elementor/editor/after_enqueue_scripts', function () {
 
  wp_enqueue_script(
 
    'sr-nested-editor',
 
    plugin_dir_url(__FILE__) . 'assets/editor/nested-editor.js',
 
    [ 'elementor-editor' ],
 
    '1.0.0',
 
    true
 
  );
 
});

Step 5: Frontend behavior (Swiper or your own slider)

Keep the slider runtime (Swiper init, navigation, pagination) on the frontend. In the editor, you usually don’t need the full slider behavior—only enough structure/classes so the layout matches.

Troubleshooting checklist

  • Does PHP get_name() for the parent exactly match getType() in the editor JS? (This is the #1 mismatch.)
  • Is the editor JS only enqueued in the Elementor editor? If it runs on the frontend, you may get console errors.
  • Are you listening for ‘elementor/nested-element-type-loaded’ before accessing $e nested exports?
  • Are your child widgets being printed inside the correct wrapper element (e.g., .swiper-wrapper)?
  • If your editor preview doesn’t update after adding/removing items, check for console errors and verify the NestedView’s onAddChild is firing.

Next ideas to level it up

  • Add per-slide controls (background image, link, CTA) and style hooks.
  • Expose slider settings on the parent (slides per view, autoplay delay, breakpoints).
  • Add editor-only helpers: slide numbering, “Add Slide” button, or a minimal preview of pagination/nav.
Back to top