Back to Articles
22 min read

Elementor Under the Hood: Core Architecture and Hook Lifecycle Analysis

Transition from implementation to engineering by decoding Elementor’s internal logic. This guide dissects the plugin’s file structure, class inheritance hierarchy, and the event-driven architecture governing the editor and frontend. We provide a comprehensive reference for critical action hooks—from `elementor/init` to `elementor/frontend/before_render`—enabling precise programmatic control over the rendering pipeline.

Elementor Architecture

Elementor File Structure

Elementor follows a modular directory structure where /includes/ contains core PHP classes, /assets/ holds CSS/JS files, /modules/ contains feature-specific code, and /core/ houses fundamental components like base classes and managers.

elementor/ ├── elementor.php # Main plugin file ├── includes/ │ ├── plugin.php # Main Plugin singleton │ ├── widgets/ # Built-in widgets │ ├── controls/ # Control types │ └── managers/ # Manager classes ├── assets/ │ ├── js/ │ └── css/ ├── core/ │ ├── base/ # Base classes │ ├── common/ │ └── editor/ └── modules/ # Feature modules

Elementor Core Classes

The Plugin class (\Elementor\Plugin) is the main singleton that bootstraps everything, accessible via \Elementor\Plugin::instance(). It initializes all managers, registers hooks, and provides access to subsystems like $plugin->widgets_manager, $plugin->controls_manager, and $plugin->elements_manager.

// Accessing core plugin instance $elementor = \Elementor\Plugin::instance(); $widgets_manager = $elementor->widgets_manager; $controls_manager = $elementor->controls_manager;

Elementor Namespaces

Elementor uses PSR-4 namespacing with the root namespace \Elementor\. Core components live under \Elementor\Core\, widgets under \Elementor\Widget\, and modules under \Elementor\Modules\{ModuleName}\. Your custom addons should use your own unique namespace.

namespace MyAddon\Widgets; use Elementor\Widget_Base; use Elementor\Controls_Manager; class My_Widget extends Widget_Base { }

Elementor Autoloader

Elementor implements a custom autoloader in includes/autoloader.php that maps class names to file paths using a naming convention: Class_Name becomes class-name.php. It registers via spl_autoload_register() and handles both core classes and module loading.

// Naming convention: // Class: \Elementor\Controls_Manager // File: includes/managers/controls.php // Class: \Elementor\Control_Text // File: includes/controls/text.php

Elementor Manager Classes

Managers follow the Registry/Factory pattern to handle collections of components. Key managers include Widgets_Manager (registers/retrieves widgets), Controls_Manager (manages control types), Elements_Manager (handles sections/columns), and Schemes_Manager. Each provides register(), unregister(), and get_*() methods.

┌─────────────────────────────────────────────────┐ │ Plugin::instance() │ ├─────────────────────────────────────────────────┤ │ widgets_manager → Widgets_Manager │ │ controls_manager → Controls_Manager │ │ elements_manager → Elements_Manager │ │ schemes_manager → Schemes_Manager │ │ skins_manager → Skins_Manager │ └─────────────────────────────────────────────────┘

Controls Stack

Controls_Stack is an abstract class that provides the foundation for registering and managing controls (settings). It handles control sections, tabs, conditions, and responsive controls. Both Element_Base and Widget_Base extend this class to inherit control registration capabilities.

// Controls Stack hierarchy abstract class Controls_Stack { protected function register_controls() {} protected function start_controls_section() {} protected function add_control() {} protected function add_responsive_control() {} protected function add_group_control() {} }

Element Base

Element_Base extends Controls_Stack and serves as the base class for all Elementor elements (sections, columns, widgets). It defines the element's name, icon, categories, and implements the render() method for frontend output and content_template() for live editor preview.

abstract class Element_Base extends Controls_Stack { abstract public function get_name(); abstract public function get_title(); protected function render() {} // PHP render protected function content_template() {} // JS template }

Widget Base

Widget_Base extends Element_Base specifically for creating widgets. It adds widget-specific functionality like icon, categories, keywords for search, and the _register_controls() method. This is the class you extend when creating custom Elementor widgets.

class My_Widget extends \Elementor\Widget_Base { public function get_name() { return 'my-widget'; } public function get_title() { return 'My Widget'; } public function get_icon() { return 'eicon-code'; } public function get_categories() { return ['general']; } protected function register_controls() { // Add controls here } protected function render() { $settings = $this->get_settings_for_display(); echo '<div>' . $settings['title'] . '</div>'; } }

Elementor Hooks

elementor/init

Fires after Elementor is fully initialized and all core components are ready. This is the safest hook for running code that depends on Elementor being fully loaded, but before widgets and controls are registered.

add_action('elementor/init', function() { // Elementor is ready, safe to use Plugin::instance() $elementor = \Elementor\Plugin::instance(); });

elementor/loaded

Fires earlier than elementor/init, right after the main Plugin class is loaded but before full initialization. Use this for very early modifications or when extending core classes before they're instantiated.

add_action('elementor/loaded', function() { // Very early hook - plugin file loaded // Core classes available but not fully initialized });

elementor/widgets/register

The primary hook for registering custom widgets with Elementor. Receives the Widgets_Manager instance as parameter. This replaced the deprecated elementor/widgets/widgets_registered hook in Elementor 3.5+.

add_action('elementor/widgets/register', function($widgets_manager) { require_once('widgets/my-widget.php'); $widgets_manager->register(new \My_Widget()); });

elementor/controls/register

Used to register custom control types with Elementor. Receives the Controls_Manager instance. Use this when you need a completely new control type beyond the built-in ones.

add_action('elementor/controls/register', function($controls_manager) { require_once('controls/my-control.php'); $controls_manager->register(new \My_Custom_Control()); });

elementor/elements/categories_registered

Fires after default widget categories are registered, allowing you to add custom categories for organizing your widgets in the editor panel. Receives the Elements_Manager instance.

add_action('elementor/elements/categories_registered', function($elements_manager) { $elements_manager->add_category('my-category', [ 'title' => __('My Widgets', 'my-addon'), 'icon' => 'fa fa-plug', ]); });

elementor/frontend/before_enqueue_scripts

Fires before Elementor enqueues its frontend JavaScript files. Use this to enqueue scripts that need to load before Elementor's frontend handlers initialize.

add_action('elementor/frontend/before_enqueue_scripts', function() { wp_enqueue_script( 'my-dependency', plugin_dir_url(__FILE__) . 'js/dependency.js', [], '1.0.0', true ); });

elementor/frontend/after_enqueue_scripts

Fires after Elementor enqueues its frontend scripts. Ideal for enqueuing widget-specific JavaScript that depends on elementorFrontend object being available.

add_action('elementor/frontend/after_enqueue_scripts', function() { wp_enqueue_script( 'my-widget-handler', plugin_dir_url(__FILE__) . 'js/handler.js', ['elementor-frontend'], '1.0.0', true ); });

elementor/frontend/before_enqueue_styles

Fires before Elementor enqueues its frontend CSS. Use for base styles that should load before Elementor's styling to ensure proper cascade order.

add_action('elementor/frontend/before_enqueue_styles', function() { wp_enqueue_style('my-base-styles', plugin_dir_url(__FILE__) . 'css/base.css' ); });

elementor/frontend/after_enqueue_styles

Fires after Elementor enqueues its frontend styles. Perfect for widget styles that may need to override Elementor defaults or depend on Elementor's CSS being loaded first.

add_action('elementor/frontend/after_enqueue_styles', function() { wp_enqueue_style('my-widget-styles', plugin_dir_url(__FILE__) . 'css/widgets.css', ['elementor-frontend'], '1.0.0' ); });

elementor/editor/before_enqueue_scripts

Fires before Elementor enqueues editor panel scripts. Use for dependencies your editor-side code requires before Elementor's editor JavaScript loads.

add_action('elementor/editor/before_enqueue_scripts', function() { wp_enqueue_script('my-editor-dependency', plugin_dir_url(__FILE__) . 'js/editor-dep.js' ); });

elementor/editor/after_enqueue_scripts

Fires after Elementor enqueues editor scripts. This is where you enqueue custom editor JavaScript for control interactions, extending the editor UI, or adding custom behaviors.

add_action('elementor/editor/after_enqueue_scripts', function() { wp_enqueue_script('my-editor-script', plugin_dir_url(__FILE__) . 'js/editor.js', ['elementor-editor'], '1.0.0', true ); });

elementor/preview/enqueue_styles

Fires when enqueuing styles specifically for the preview iframe within the editor. Use this for styles that should only appear in the editor preview, not the live frontend.

add_action('elementor/preview/enqueue_styles', function() { wp_enqueue_style('my-preview-styles', plugin_dir_url(__FILE__) . 'css/preview.css' ); });

elementor/frontend/before_render

Fires before an element renders its content on the frontend. Receives the element instance, allowing you to modify settings or add wrapper markup before output.

add_action('elementor/frontend/before_render', function($element) { if ('my-widget' === $element->get_name()) { echo '<!-- Widget Start -->'; } });

elementor/frontend/after_render

Fires after an element completes rendering. Useful for closing wrappers, adding tracking pixels, or cleanup operations after widget output.

add_action('elementor/frontend/after_render', function($element) { if ('my-widget' === $element->get_name()) { echo '<!-- Widget End -->'; } });

elementor/widget/before_render_content

Fires before the widget's inner content renders (after the widget wrapper opens). Allows injecting content inside the widget wrapper but before the actual widget output.

add_action('elementor/widget/before_render_content', function($widget) { if ('heading' === $widget->get_name()) { echo '<div class="custom-inner-wrapper">'; } });

elementor/widget/render_content

Filter hook that allows modifying the final rendered content of a widget. Receives the content string and widget instance, must return the modified content.

add_filter('elementor/widget/render_content', function($content, $widget) { if ('text-editor' === $widget->get_name()) { $content = str_replace('foo', 'bar', $content); } return $content; }, 10, 2);

elementor/element/before_section_start

Fires before a control section starts, allowing you to inject additional sections before existing ones. Receives element instance, section ID, and arguments.

add_action('elementor/element/before_section_start', function($element, $section_id, $args) { if ('heading' === $element->get_name() && 'section_title' === $section_id) { $element->start_controls_section('my_section', [ 'label' => __('My Section', 'my-addon'), ]); // Add controls $element->end_controls_section(); } }, 10, 3);

elementor/element/after_section_start

Fires right after a section starts (inside the section). Use this to inject additional controls at the beginning of an existing section.

// Pattern: elementor/element/{element_name}/{section_id}/after_section_start add_action('elementor/element/heading/section_title/after_section_start', function($element, $args) { $element->add_control('my_control', [ 'label' => __('My Control', 'my-addon'), 'type' => \Elementor\Controls_Manager::TEXT, ]); }, 10, 2 );

elementor/element/before_section_end

Fires just before a section ends. Perfect for appending controls at the end of an existing section without creating a new one.

// Pattern: elementor/element/{element_name}/{section_id}/before_section_end add_action('elementor/element/image/section_image/before_section_end', function($element, $args) { $element->add_control('custom_attribute', [ 'label' => __('Custom Attr', 'my-addon'), 'type' => \Elementor\Controls_Manager::TEXT, ]); }, 10, 2 );

elementor/element/after_section_end

Fires after a section completely ends. Use this to add entirely new sections immediately after an existing section closes.

add_action('elementor/element/button/section_style/after_section_end', function($element, $args) { $element->start_controls_section('custom_section', [ 'label' => __('Custom Styles', 'my-addon'), 'tab' => \Elementor\Controls_Manager::TAB_STYLE, ]); // Add controls $element->end_controls_section(); }, 10, 2 );

elementor/element/parse_css

Filter for modifying the CSS generated for an element. Receives the Post_CSS instance and element, allowing dynamic CSS injection based on control values.

add_action('elementor/element/parse_css', function($post_css, $element) { $settings = $element->get_settings(); if (!empty($settings['my_custom_color'])) { $css = '.elementor-element-' . $element->get_id() . ' {'; $css .= 'border-color: ' . $settings['my_custom_color'] . ';'; $css .= '}'; $post_css->get_stylesheet()->add_raw_css($css); } }, 10, 2);

elementor/db/before_save

Fires before Elementor saves page data to the database. Receives the post ID and editor data array. Use for validation, data transformation, or triggering pre-save actions.

add_action('elementor/db/before_save', function($post_id, $data) { // Validate or modify data before save error_log("Saving Elementor data for post: $post_id"); }, 10, 2);

elementor/db/after_save

Fires after Elementor successfully saves page data. Ideal for cache clearing, triggering external syncs, or post-save processing like updating related content.

add_action('elementor/db/after_save', function($post_id, $data) { // Clear cache, notify external services, etc. wp_cache_delete('elementor_data_' . $post_id); do_action('my_custom_cache_clear', $post_id); }, 10, 2);

Elementor JavaScript Hooks

Frontend Hooks

Elementor frontend uses jQuery-based hooks via elementorFrontend.hooks. Common hooks include frontend/element_ready/widget (fires when any widget becomes ready) and frontend/element_ready/{widget.name} for specific widgets.

// Frontend hook for widget initialization jQuery(window).on('elementor/frontend/init', function() { elementorFrontend.hooks.addAction( 'frontend/element_ready/my-widget.default', function($scope) { // $scope is jQuery element wrapper console.log('My widget ready:', $scope); $scope.find('.my-slider').slick(); } ); });

Editor Hooks

Editor-side hooks use elementor.hooks for extending editor functionality. Key hooks include panel/open_editor/{elementType}/{elementName} and various model/view lifecycle hooks.

// Editor hook example jQuery(window).on('elementor:init', function() { elementor.hooks.addAction( 'panel/open_editor/widget/heading', function(panel, model, view) { console.log('Heading widget panel opened'); } ); elementor.hooks.addFilter( 'editor/style/styleText', function(css, context) { return css + '/* Custom CSS */'; } ); });

elementorFrontend Object

The global elementorFrontend object is available on the frontend and provides access to handlers, hooks, and utilities. Key properties include config, hooks, elementsHandler, and methods like waypoint() and isEditMode().

// elementorFrontend structure /* elementorFrontend = { config: { ... }, hooks: Hooks, elementsHandler: ElementsHandler, isEditMode: function(), getElements: function(type), waypoint: function($element, callback), Module: function() { ... } } */ // Usage example jQuery(window).on('elementor/frontend/init', function() { if (elementorFrontend.isEditMode()) { console.log('In editor preview'); } var settings = elementorFrontend.config.settings; });

elementor Object (Editor)

The global elementor object in the editor context provides full access to the editor's Backbone/Marionette architecture. Includes channels, hooks, modules, and access to the document model for programmatic editing.

// elementor editor object structure /* elementor = { hooks: Hooks, channels: { editor: Backbone.Radio.channel, data: Backbone.Radio.channel, panelElements: Backbone.Radio.channel }, getPreviewView: function(), getPanelView: function(), settings: SettingsModule, modules: { ... } } */ // Usage in editor context jQuery(window).on('elementor:init', function() { // Listen to element changes elementor.channels.editor.on('change', function() { console.log('Editor changed'); }); // Access current document var elements = elementor.elements.toJSON(); });

Quick Reference Diagram

┌─────────────────────────────────────────────────────────────────┐ │ ELEMENTOR LIFECYCLE │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ WordPress Init │ │ │ │ │ ▼ │ │ ┌─────────────────┐ │ │ │ elementor/loaded│ ← Plugin file loaded │ │ └────────┬────────┘ │ │ ▼ │ │ ┌─────────────────┐ │ │ │ elementor/init │ ← Core initialized │ │ └────────┬────────┘ │ │ ▼ │ │ ┌──────────────────────────────┐ │ │ │ elementor/widgets/register │ ← Register widgets │ │ │ elementor/controls/register │ ← Register controls │ │ │ elementor/elements/categories│ ← Register categories │ │ └────────┬─────────────────────┘ │ │ ▼ │ │ ┌─────────────────────────────────────┐ │ │ │ FRONTEND │ EDITOR │ │ │ ├───────────────────┼─────────────────┤ │ │ │ before_enqueue_* │ editor/before_* │ │ │ │ after_enqueue_* │ editor/after_* │ │ │ │ before_render │ preview/* │ │ │ │ after_render │ │ │ │ └───────────────────┴─────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘