elementor-development
Elementor Addon & Widget Development
Consolidated reference for addon architecture, widget creation, manager registration, scripts/styles, data structure, deprecations, and CLI commands.
See also:
- Widget Rendering Details -- full control registration, render(), content_template(), render attributes
- Data Structure, Deprecations & CLI -- JSON format, element structure, deprecation timeline, CLI commands
1. Addon Structure
Plugin Header
Every Elementor addon requires standard WordPress headers plus optional Elementor headers.
<?php
/**
* Plugin Name: Elementor Test Addon
* Description: Custom Elementor addon.
* Plugin URI: https://elementor.com/
* Version: 1.0.0
* Author: Elementor Developer
* Author URI: https://developers.elementor.com/
* Text Domain: elementor-test-addon
* Requires Plugins: elementor
*
* Elementor tested up to: 3.25.0
* Elementor Pro tested up to: 3.25.0
*/
defined( 'ABSPATH' ) || exit;
function elementor_test_addon() {
require_once __DIR__ . '/includes/plugin.php';
\Elementor_Test_Addon\Plugin::instance();
}
add_action( 'plugins_loaded', 'elementor_test_addon' );
Main Class (Singleton + Compatibility Checks)
namespace Elementor_Test_Addon;
final class Plugin {
const VERSION = '1.0.0';
const MINIMUM_ELEMENTOR_VERSION = '3.20.0';
const MINIMUM_PHP_VERSION = '7.4';
private static $_instance = null;
public static function instance() {
if ( is_null( self::$_instance ) ) {
self::$_instance = new self();
}
return self::$_instance;
}
public function __construct() {
if ( $this->is_compatible() ) {
add_action( 'elementor/init', [ $this, 'init' ] );
}
}
public function is_compatible(): bool {
if ( ! did_action( 'elementor/loaded' ) ) {
add_action( 'admin_notices', [ $this, 'admin_notice_missing_main_plugin' ] );
return false;
}
if ( ! version_compare( ELEMENTOR_VERSION, self::MINIMUM_ELEMENTOR_VERSION, '>=' ) ) {
add_action( 'admin_notices', [ $this, 'admin_notice_minimum_elementor_version' ] );
return false;
}
if ( version_compare( PHP_VERSION, self::MINIMUM_PHP_VERSION, '<' ) ) {
add_action( 'admin_notices', [ $this, 'admin_notice_minimum_php_version' ] );
return false;
}
return true;
}
public function init(): void {
add_action( 'elementor/widgets/register', [ $this, 'register_widgets' ] );
add_action( 'elementor/controls/register', [ $this, 'register_controls' ] );
}
public function register_widgets( $widgets_manager ): void {
require_once __DIR__ . '/widgets/widget-1.php';
$widgets_manager->register( new \Elementor_Widget_1() );
}
public function register_controls( $controls_manager ): void {
require_once __DIR__ . '/controls/control-1.php';
$controls_manager->register( new \Elementor_Control_1() );
}
}
Folder Structure
elementor-test-addon/
elementor-test-addon.php # Main file with headers
includes/
plugin.php # Main class (singleton)
widgets/ # Widget classes
controls/ # Custom controls
dynamic-tags/ # Dynamic tag classes
finder/ # Finder category classes
assets/
js/ # Frontend/editor JS
css/ # Frontend/editor CSS
images/
2. Widget Development
Widget Class Skeleton
class Elementor_Test_Widget extends \Elementor\Widget_Base {
// --- Required ---
public function get_name(): string {
return 'test_widget';
}
public function get_title(): string {
return esc_html__( 'Test Widget', 'textdomain' );
}
public function get_icon(): string {
return 'eicon-code';
}
public function get_categories(): array {
return [ 'general' ];
}
// --- Optional ---
public function get_keywords(): array {
return [ 'test', 'example' ];
}
public function get_custom_help_url(): string {
return 'https://example.com/widget-help';
}
public function get_script_depends(): array {
return [ 'widget-custom-script' ];
}
public function get_style_depends(): array {
return [ 'widget-custom-style' ];
}
public function has_widget_inner_wrapper(): bool {
return false; // DOM optimization: single wrapper
}
protected function is_dynamic_content(): bool {
return false; // Enable output caching for static content
}
protected function get_upsale_data(): array {
return [
'condition' => ! \Elementor\Utils::has_pro(),
'image' => esc_url( ELEMENTOR_ASSETS_URL . 'images/go-pro.svg' ),
'image_alt' => esc_attr__( 'Upgrade', 'textdomain' ),
'title' => esc_html__( 'Promotion heading', 'textdomain' ),
'description' => esc_html__( 'Get the premium version.', 'textdomain' ),
'upgrade_url' => esc_url( 'https://example.com/upgrade-to-pro/' ),
'upgrade_text' => esc_html__( 'Upgrade Now', 'textdomain' ),
];
}
protected function register_controls(): void { /* see resources/widget-rendering.md */ }
protected function render(): void { /* see resources/widget-rendering.md */ }
protected function content_template(): void { /* see resources/widget-rendering.md */ }
}
Register Custom Widget Category
function add_elementor_widget_categories( $elements_manager ) {
$elements_manager->add_category( 'my-category', [
'title' => esc_html__( 'My Category', 'textdomain' ),
'icon' => 'fa fa-plug',
] );
}
add_action( 'elementor/elements/categories_registered', 'add_elementor_widget_categories' );
Selector Tokens
| Token | Description |
|---|---|
{{WRAPPER}} |
Widget wrapper element |
{{VALUE}} |
Control value |
{{UNIT}} |
Unit control value |
{{URL}} |
URL from media control |
{{SELECTOR}} |
Group control CSS selector |
Inline Editing Toolbars
| Mode | Toolbar | Use Case |
|---|---|---|
'none' |
No toolbar | Plain text headings |
'basic' |
Bold, italic, underline | Short descriptions |
'advanced' |
Full (links, headings, lists) | Rich text content |
3. Manager Registration
Registration Hooks Reference
| Component | Hook | Manager Type | Method |
|---|---|---|---|
| Widgets | elementor/widgets/register |
\Elementor\Widgets_Manager |
register() / unregister() |
| Controls | elementor/controls/register |
\Elementor\Controls_Manager |
register() / unregister() |
| Dynamic Tags | elementor/dynamic_tags/register |
\Elementor\Core\DynamicTags\Manager |
register() / unregister() |
| Finder | elementor/finder/register |
Categories_Manager |
register() / unregister() |
| Categories | elementor/elements/categories_registered |
Elements_Manager |
add_category() |
Register Widgets
function register_new_widgets( $widgets_manager ) {
require_once __DIR__ . '/widgets/widget-1.php';
$widgets_manager->register( new \Elementor_Widget_1() );
}
add_action( 'elementor/widgets/register', 'register_new_widgets' );
Unregister Widgets
function unregister_widgets( $widgets_manager ) {
$widgets_manager->unregister( 'heading' );
$widgets_manager->unregister( 'image' );
}
add_action( 'elementor/widgets/register', 'unregister_widgets' );
Register/Unregister Controls
function register_new_controls( $controls_manager ) {
require_once __DIR__ . '/controls/control-1.php';
$controls_manager->register( new \Elementor_Control_1() );
}
add_action( 'elementor/controls/register', 'register_new_controls' );
function unregister_controls( $controls_manager ) {
$controls_manager->unregister( 'control-1' );
}
add_action( 'elementor/controls/register', 'unregister_controls' );
Register/Unregister Dynamic Tags
function register_dynamic_tags( $dynamic_tags_manager ) {
require_once __DIR__ . '/dynamic-tags/tag-1.php';
$dynamic_tags_manager->register( new \Elementor_Dynamic_Tag_1() );
}
add_action( 'elementor/dynamic_tags/register', 'register_dynamic_tags' );
function unregister_dynamic_tags( $dynamic_tags_manager ) {
$dynamic_tags_manager->unregister( 'dynamic-tag-1' );
}
add_action( 'elementor/dynamic_tags/register', 'unregister_dynamic_tags' );
Register/Unregister Finder Categories
function register_finder_categories( $finder_manager ) {
require_once __DIR__ . '/finder/finder-1.php';
$finder_manager->register( new \Elementor_Finder_Category_1() );
}
add_action( 'elementor/finder/register', 'register_finder_categories' );
function unregister_finder_categories( $finder_manager ) {
$finder_manager->unregister( 'finder-category-1' );
}
add_action( 'elementor/finder/register', 'unregister_finder_categories' );
4. Scripts & Styles
Frontend Hooks
| Hook | Purpose |
|---|---|
elementor/frontend/before_register_scripts |
Register scripts before Elementor |
elementor/frontend/after_register_scripts |
Register scripts after Elementor |
elementor/frontend/before_enqueue_scripts |
Enqueue scripts before Elementor |
elementor/frontend/after_enqueue_scripts |
Enqueue scripts after Elementor |
elementor/frontend/before_register_styles |
Register styles before Elementor |
elementor/frontend/after_register_styles |
Register styles after Elementor |
elementor/frontend/before_enqueue_styles |
Enqueue styles before Elementor |
elementor/frontend/after_enqueue_styles |
Enqueue styles after Elementor |
Editor Hooks
| Hook | Purpose |
|---|---|
elementor/editor/before_enqueue_scripts |
Enqueue editor scripts (before) |
elementor/editor/after_enqueue_scripts |
Enqueue editor scripts (after) |
elementor/editor/before_enqueue_styles |
Enqueue editor styles (before) |
elementor/editor/after_enqueue_styles |
Enqueue editor styles (after) |
Preview Hooks
| Hook | Purpose |
|---|---|
elementor/preview/enqueue_scripts |
Enqueue preview scripts |
elementor/preview/enqueue_styles |
Enqueue preview styles |
Frontend Registration Pattern
function my_plugin_frontend_scripts() {
wp_register_script( 'my-widget-script', plugins_url( 'assets/js/widget.js', __FILE__ ) );
wp_register_style( 'my-widget-style', plugins_url( 'assets/css/widget.css', __FILE__ ) );
}
add_action( 'wp_enqueue_scripts', 'my_plugin_frontend_scripts' );
Widget-Level Dependencies
Declare in the widget class; Elementor loads them only when the widget is used.
public function get_script_depends(): array {
return [ 'my-widget-script', 'external-library' ];
}
public function get_style_depends(): array {
return [ 'my-widget-style', 'external-framework' ];
}
Control-Level Enqueue
class My_Control extends \Elementor\Base_Control {
protected function enqueue(): void {
wp_enqueue_script( 'control-script' );
wp_enqueue_style( 'control-style' );
}
}
8. Common Mistakes
| Mistake | Fix |
|---|---|
Using elementor/widgets/widgets_registered hook |
Use elementor/widgets/register (old hook deprecated) |
Calling register_widget_type() |
Use register() on the widgets manager |
Using scheme for colors/typography |
Use global with Global_Colors/Global_Typography constants |
Using _register_controls() with underscore prefix |
Use register_controls() (no underscore) |
Skipping did_action('elementor/loaded') check |
Always verify Elementor is loaded before using its classes |
Missing Requires Plugins: elementor header |
Add it so WordPress enforces Elementor dependency |
Using {{ }} for HTML output in JS templates |
Use {{{ }}} (triple) for unescaped HTML; {{ }} escapes output |
| Not declaring widget script/style dependencies | Implement get_script_depends() / get_style_depends() |
| Enqueueing scripts globally instead of per-widget | Register with wp_register_script, declare via get_script_depends() |
Using innerHTML = in editor JS |
Use Elementor template syntax or DOM methods |
Not using esc_html__() for translatable strings |
Always wrap user-visible strings in localization functions |
Missing defined('ABSPATH') || exit; guard |
Add to every PHP file to prevent direct access |
Using has_widget_inner_wrapper returning true without need |
Return false to reduce DOM nodes (optimization) |
Not implementing content_template() |
Without it, editor preview requires server round-trip on every change |
Using add_render_attribute inside content_template() |
Use view.addRenderAttribute() in JS templates |
More from peixotorms/odinlayer-skills
elementor-hooks
Use when hooking into Elementor lifecycle events, injecting controls, filtering widget output, or using the JS APIs. Covers elementor/init, elementor/element/before_section_end, elementor/element/after_section_end, elementor/widget/render_content filter, elementor/frontend/after_enqueue_styles, frontend JS hooks (elementorFrontend.hooks, frontend/element_ready), editor JS hooks (elementor.hooks), $e.commands API ($e.run, $e.commands.register), $e.routes, $e.hooks (registerUIBefore, registerUIAfter), control injection patterns, CSS file hooks, forms hooks (Pro), and query filters.
26elementor-themes
Use when building Elementor-compatible themes, registering theme locations, creating dynamic tags, or extending the Finder. Covers register_location, theme_builder locations, elementor_theme_do_location, Theme_Document and theme conditions, Tag_Base for dynamic tags (register_tag, get_value, render), Finder extension (Category_Base, register via elementor/finder/register), Context_Menu customization (elements/context-menu/groups filter), Hello Elementor theme (elementor-hello-theme, hello_elementor_* filters), and hosting page cache integration hooks.
25elementor-controls
Use when adding controls to Elementor widgets, creating custom controls, or referencing control type parameters. Covers add_control with types (TEXT, SELECT, SLIDER, COLOR, MEDIA, REPEATER, CHOOSE, NUMBER, SWITCHER, URL, ICONS), TYPOGRAPHY and BACKGROUND group controls, BORDER, BOX_SHADOW group controls, add_responsive_control, add_group_control, CSS selectors ({{WRAPPER}}, {{VALUE}}), condition and conditions for conditional display, dynamic content tags, POPOVER_TOGGLE, and global styles integration.
16elementor-forms
Use when creating custom Elementor form actions, custom form field types, form validation, or processing form submissions. Covers Elementor Pro forms (ElementorPro\Modules\Forms), Action_Base (get_name, get_label, run, register_settings_section, on_export), after_submit processing, Field_Base (field_type, render field HTML, validation callback, update_controls), content_template for editor preview, form action registration, export_type handling, update_record patterns, elementor_pro/forms/validation hook, email filters, and webhook response handling.
12flyonui
Use when building with FlyonUI — Tailwind CSS component library with CSS classes and optional JS plugins. Covers CSS component classes, JS plugin system (accordion, carousel, collapse, combobox, datatable, dropdown, select, tabs, tooltip, etc.), theming, installation, class reference, and plugin initialization via MCP tools.
9wp-javascript
Use when working with JavaScript in WordPress plugins or themes. Covers wp_enqueue_script, wp_localize_script, wp_add_inline_script, jQuery in WordPress (noConflict mode, $.ajax), AJAX handlers (wp_ajax_, admin-ajax.php, wp_create_nonce, check_ajax_referer), wp.ajax, wp.apiFetch (wp-api-fetch), wp-util and wp.template (Underscore templates), Heartbeat API, script dependencies, defer/async loading strategies (WordPress 6.3+), wp_set_script_translations, and frontend-backend communication patterns.
7