| name | elementor-widgets |
| description | How to create, register, and develop custom Elementor widgets for WordPress plugins. Use this skill whenever the user wants to build a custom Elementor widget, add controls to a widget, render widget output, handle widget dependencies, or optimize widget performance. Trigger on any mention of: Elementor widget, Widget_Base, register_controls, elementor/widgets/register, get_name/get_title/get_icon, add_control, render(), content_template, repeater field, widget caching, DOM optimization, or creating a WordPress plugin that integrates with Elementor.
|
Elementor Widget Development
Overview
An Elementor widget is a PHP class extending \Elementor\Widget_Base, registered via a WordPress action hook. It has three main phases:
- Data — metadata about the widget (name, title, icon, categories)
- Controls — input fields users configure in the editor
- Rendering — PHP (frontend) and JS (editor preview) templates
File / Folder Structure
my-elementor-plugin/
├── widgets/
│ └── my-widget.php ← the widget class
└── my-elementor-plugin.php ← plugin entry, registers the widget
1. Plugin Entry File
<?php
if ( ! defined( 'ABSPATH' ) ) exit;
function register_my_widgets( \Elementor\Widgets_Manager $widgets_manager ): void {
require_once __DIR__ . '/widgets/my-widget.php';
$widgets_manager->register( new \My_Elementor_Widget() );
}
add_action( 'elementor/widgets/register', 'register_my_widgets' );
Why elementor/widgets/register? This is the correct hook — it fires after Elementor is ready and provides the widgets manager so you can call ->register().
2. Widget Skeleton
<?php
class My_Elementor_Widget extends \Elementor\Widget_Base {
public function get_name(): string { return 'my-widget'; }
public function get_title(): string { return esc_html__( 'My Widget', 'textdomain' ); }
public function get_icon(): string { return 'eicon-code'; }
public function get_categories(): array { return [ 'general' ]; }
public function get_keywords(): array { return [ 'my', 'widget' ]; }
public function get_custom_help_url(): string { return 'https://example.com/docs'; }
public function get_script_depends(): array { return [ 'my-script-handle' ]; }
public function get_style_depends(): array { return [ 'my-style-handle' ]; }
protected function register_controls(): void { }
protected function render(): void { }
protected function content_template(): void { }
}
3. Controls (register_controls)
Controls must be wrapped in sections. Sections live in tabs (TAB_CONTENT or TAB_STYLE).
protected function register_controls(): void {
$this->start_controls_section( 'section_content', [
'label' => esc_html__( 'Content', 'textdomain' ),
'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
] );
$this->add_control( 'title', [
'label' => esc_html__( 'Title', 'textdomain' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => esc_html__( 'Hello World', 'textdomain' ),
'placeholder' => esc_html__( 'Enter title', 'textdomain' ),
] );
$this->end_controls_section();
$this->start_controls_section( 'section_style', [
'label' => esc_html__( 'Style', 'textdomain' ),
'tab' => \Elementor\Controls_Manager::TAB_STYLE,
] );
$this->add_control( 'title_color', [
'label' => esc_html__( 'Color', 'textdomain' ),
'type' => \Elementor\Controls_Manager::COLOR,
'selectors' => [
'{{WRAPPER}} .my-title' => 'color: {{VALUE}};',
],
] );
$this->end_controls_section();
}
See references/controls.md for the full list of control types and their parameters.
Conditional Controls
Show a control only when another has a specific value:
$this->add_control( 'custom_marker', [
'label' => esc_html__( 'Custom Marker', 'textdomain' ),
'type' => \Elementor\Controls_Manager::TEXT,
'condition' => [ 'marker_type' => 'custom' ],
] );
Repeater Controls
Use \Elementor\Repeater to let users add/remove dynamic rows:
$repeater = new \Elementor\Repeater();
$repeater->add_control( 'item_title', [
'label' => esc_html__( 'Title', 'textdomain' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => esc_html__( 'Item', 'textdomain' ),
] );
$this->add_control( 'items_list', [
'label' => esc_html__( 'Items', 'textdomain' ),
'type' => \Elementor\Controls_Manager::REPEATER,
'fields' => $repeater->get_controls(),
'default' => [ [ 'item_title' => 'Item 1' ] ],
'title_field' => '{{{ item_title }}}',
] );
4. Rendering
PHP Render (frontend)
protected function render(): void {
$settings = $this->get_settings_for_display();
echo '<h2 class="my-title">' . esc_html( $settings['title'] ) . '</h2>';
$this->add_link_attributes( 'my_link', $settings['link'] );
echo '<a ' . $this->get_render_attribute_string( 'my_link' ) . '>Click</a>';
foreach ( $settings['items_list'] as $index => $item ) {
$tag = $this->get_repeater_setting_key( 'item_title', 'items_list', $index );
$this->add_render_attribute( $tag, 'class', 'elementor-repeater-item-' . $item['_id'] );
echo '<div ' . $this->get_render_attribute_string( $tag ) . '>';
echo esc_html( $item['item_title'] );
echo '</div>';
}
}
JS Content Template (editor preview)
protected function content_template(): void {
?>
<
var title = settings.title;
<h2 class="my-title" style="color: {{ settings.title_color }};">{{{ title }}}</h2>
<?php
}
- Use
{{{ }}} for unescaped HTML, {{ }} for escaped text.
- Access controls via
settings.control_name.
5. Script & Style Dependencies
Register assets in WordPress before declaring them as widget dependencies:
function my_widget_assets(): void {
wp_register_script( 'my-script-handle', plugin_dir_url(__FILE__) . 'assets/my-script.js', [ 'jquery' ], '1.0.0', true );
wp_register_style( 'my-style-handle', plugin_dir_url(__FILE__) . 'assets/my-style.css', [], '1.0.0' );
}
add_action( 'wp_enqueue_scripts', 'my_widget_assets' );
add_action( 'elementor/frontend/after_register_scripts', 'my_widget_assets' );
Then in the widget class:
public function get_script_depends(): array { return [ 'my-script-handle' ]; }
public function get_style_depends(): array { return [ 'my-style-handle' ]; }
Elementor will enqueue these only on pages where the widget is used.
6. Optimization
DOM Optimization (remove inner wrapper div)
New widgets should opt out of the legacy inner wrapper to reduce DOM size:
public function has_widget_inner_wrapper(): bool { return false; }
This changes the markup from:
<div class="elementor-widget elementor-widget-my-widget">
<div class="elementor-widget-container"></div>
</div>
to the leaner:
<div class="elementor-widget elementor-widget-my-widget"></div>
Do NOT set this on existing widgets that use .elementor-widget-container in their CSS — it will break styles.
Output Caching
For widgets with static output (same HTML for all users every time), enable caching to reduce server load and improve TTFB:
public function is_dynamic_content(): bool { return false; }
Never set is_dynamic_content() to false if the widget output:
- Depends on the current user (logged-in state, roles)
- Depends on time or random values
- Pulls live external data
- Uses WooCommerce cart/session data
7. Widget Categories
Built-in categories: general, basic, pro-elements, woocommerce-elements, theme-elements, site-settings.
To register a custom category:
add_action( 'elementor/elements/categories_registered', function( $elements_manager ) {
$elements_manager->add_category( 'my-category', [
'title' => esc_html__( 'My Widgets', 'textdomain' ),
'icon' => 'fa fa-plug',
] );
} );
Then use 'categories' => [ 'my-category' ] in the widget.
Reference Files
references/controls.md — All available control types with parameters and examples
references/icons.md — Common Elementor icon names for get_icon()
Quick Checklist
When building a widget, ensure you have: