Build a Fully Working Elementor Addon (Custom Widget Plugin)
1. Overview
An Elementor addon is essentially a WordPress plugin that registers custom widgets into Elementor’s editor panel.
At a minimum, your addon must:
Register itself with WordPress
Hook into Elementor lifecycle
Define one or more widget classes
Register controls (inputs)
Render output (frontend + editor preview)
2. Plugin Structure
Create this folder:
/wp-content/plugins/my-elementor-addon/
Inside:
my-elementor-addon/
│
├── my-elementor-addon.php
├── widgets/
│ └── hello-widget.php
└── assets/
├── css/
│ └── style.css
└── js/
└── script.js
3. Main Plugin File
📄 File: my-elementor-addon.php
✅ Purpose
Bootstraps plugin, checks Elementor, registers widgets.
<?php
/**
* Plugin Name: My Elementor Addon
* Description: Custom Elementor widgets
* Version: 1.0
* Author: Your Name
*/
if (!defined('ABSPATH')) exit;
// Check if Elementor is loaded
function mea_check_elementor_loaded() {
if (!did_action('elementor/loaded')) {
add_action('admin_notices', function() {
echo '<div class="notice notice-warning"><p>Elementor not installed.</p></div>';
});
return;
}
}
add_action('plugins_loaded', 'mea_check_elementor_loaded');
// Register Widget
function mea_register_widgets($widgets_manager) {
require_once(__DIR__ . '/widgets/hello-widget.php');
$widgets_manager->register(new \MEA_Hello_Widget()); } add_action('elementor/widgets/register', 'mea_register_widgets');
// Register Assets
function mea_register_assets() {
wp_register_style('mea-style', plugins_url('/assets/css/style.css', FILE)); wp_register_script('mea-script', plugins_url('/assets/js/script.js', FILE), ['jquery'], false, true); }
add_action('wp_enqueue_scripts', 'mea_register_assets');
🔍 Explanation
| Section | Role |
|---|---|
| Plugin header | Required for WordPress recognition |
did_action() |
Ensures Elementor is active |
elementor/widgets/register |
Hook to register widgets |
wp_register_style/script |
Prepares frontend assets |
4. Create Your First Widget
📄 File: widgets/hello-widget.php
<?php
if (!defined('ABSPATH')) exit;
class MEA_Hello_Widget extends \Elementor\Widget_Base {
// Widget internal name
public function get_name() {
return 'hello_widget';
}
// Widget display title
public function get_title() {
return 'Hello Widget';
}
// Widget icon in Elementor panel
public function get_icon() {
return 'eicon-code';
}
// Widget category
public function get_categories() {
return ['general'];
}
🎛️ Controls (User Inputs)
📌 Purpose
Defines editable UI fields in Elementor panel.
protected function register_controls() {
$this->start_controls_section(
'content_section',
[
'label' => 'Content',
'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
]
);
// Text Input
$this->add_control(
'title',
[
'label' => 'Title',
'type' => \Elementor\Controls_Manager::TEXT,
'default' => 'Hello World',
]
);
// Color Picker
$this->add_control(
'color',
[
'label' => 'Text Color',
'type' => \Elementor\Controls_Manager::COLOR,
'default' => '#000000',
]
);
$this->end_controls_section();
}
🖥️ Frontend Rendering
📌 Purpose
Outputs HTML seen on the live website.
protected function render() {
\(settings = \)this->get_settings_for_display();
echo '<h2 style="color:' . esc_attr($settings['color']) . ';">';
echo esc_html($settings['title']);
echo '</h2>';
}
⚡ Editor Live Preview (JS Template)
📌 Purpose
Shows real-time preview inside Elementor editor.
protected function content_template() {
?>
<#
var color = settings.color;
var title = settings.title;
#>
<h2 style="color: {{ color }}">{{{ title }}}</h2>
<?php
}
}
5. Add Styling
📄 File: assets/css/style.css
.mea-widget {
padding: 20px;
border: 1px solid #ddd;
text-align: center;
}
6. Add Script (Optional)
📄 File: assets/js/script.js
jQuery(document).ready(function($) {
console.log("Elementor addon loaded");
});
7. Enqueue Assets in Widget (Best Practice)
Modify widget class:
public function get_style_depends() {
return ['mea-style'];
}
public function get_script_depends() {
return ['mea-script'];
}
8. Final Result
After activating plugin:
Go to Elementor editor
Search: Hello Widget
Drag & drop
Edit text + color live
9. Advanced Enhancements
🔧 Add More Controls
$this->add_control(
'alignment',
[
'label' => 'Alignment',
'type' => \Elementor\Controls_Manager::CHOOSE,
'options' => [
'left' => ['title' => 'Left', 'icon' => 'eicon-text-align-left'],
'center' => ['title' => 'Center', 'icon' => 'eicon-text-align-center'],
'right' => ['title' => 'Right', 'icon' => 'eicon-text-align-right'],
],
'default' => 'center',
]
);
🧩 Add Repeater (Dynamic Lists)
$repeater = new \Elementor\Repeater();
$repeater->add_control(
'list_text',
[
'label' => 'Item Text',
'type' => \Elementor\Controls_Manager::TEXT,
]
);
$this->add_control(
'list',
[
'label' => 'List Items',
'type' => \Elementor\Controls_Manager::REPEATER,
'fields' => $repeater->get_controls(),
]
);
🎨 Add Style Tab Controls
$this->start_controls_section(
'style_section',
[
'label' => 'Style',
'tab' => \Elementor\Controls_Manager::TAB_STYLE,
]
);
$this->add_control(
'bg_color',
[
'label' => 'Background',
'type' => \Elementor\Controls_Manager::COLOR,
'selectors' => [
'{{WRAPPER}}' => 'background-color: {{VALUE}};',
],
]
);
$this->end_controls_section();
10. Common Pitfalls
| Issue | Cause |
|---|---|
| Widget not showing | Wrong hook (elementor/widgets/register) |
| Fatal error | Missing use Elementor\Widget_Base namespace |
| No styles | Not enqueued properly |
| Live preview broken | Missing content_template() |
11. Production Best Practices
Use PSR-4 autoloading instead of manual
requireSplit widgets into multiple files
Use namespaces:
namespace MEA\Widgets;
Add security escaping:
esc_html()esc_attr()
Use translatable strings:
__('Hello Widget', 'mea')
12. Scaling Your Addon
Once basics work, you can evolve into:
Widget packs (10–50 widgets)
Dynamic data integrations (API, AI, etc.)
Custom Elementor controls
WooCommerce widgets
13. Mental Model (Important)
Think of Elementor widgets as:
Controls (UI config)
↓
Settings array
↓
Render() → HTML output
↓
Styled via CSS + dynamic selectors
14. Minimal Working Summary
If you strip everything down:
Plugin file loads widget
Widget defines:
name
controls
render()
That alone = fully working Elementor addon.


