Elementor V3 widget development
SkillMediaBuilds and reviews production-ready classic Elementor widgets based on `Elementor\Widget_Base`: companion-plugin bootstrap and compatibility gates, `elementor/widgets/register`, widget identity, PHP and editor rendering, render attributes, asset dependencies, frontend handlers, accessibility, output caching, and regression tests. Use when code extends `Widget_Base`, implements `register_controls()` or `render()`, registers an Elementor widget/category, or must distinguish the established V3 widget API from Atomic Widgets / Editor V4. Does not cover custom control types.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Elementor V3 widget development skill
What this skill tells your AI
The instructions your AI receives, as published by lonsdale201/wp-agent-skills in elementor/elementor-v3-widget-development/SKILL.md and read by ahel’s review.
Build companion-plugin widgets on Elementor's established Widget_Base / Controls_Stack architecture. Treat V3 here as the classic editor/widget model, not as an installed Elementor 3.x version: this API remains available and is used by core widgets in Elementor 4.2.3.
Do not mix this model with Atomic Widgets / Editor V4. Atomic widgets extend classes under Elementor\Modules\AtomicWidgets, declare prop types and styles differently, and remain a moving surface. Never translate a V3 control array into an Atomic schema by guesswork.
When to use this skill
- Create or review a class extending
\Elementor\Widget_Base. - Register widgets, categories, scripts, or styles from an Elementor addon.
- Implement
register_controls(),render(),content_template(), orrender_plain_content(). - Add widget frontend JavaScript through
frontend/element_ready/{widget-name}.default. - Decide whether
is_dynamic_content()may returnfalse. - Diagnose a widget visible in PHP but missing/broken in the editor or frontend.
- Migrate
_register_controls()orelementor/widgets/widgets_registeredto current APIs.
For a complete companion-plugin skeleton and test matrix, read references/widget-contract-and-example.md. Load elementor-v3-widget-controls as well when designing or reviewing the control schema.
Architecture boundary
Use the following identity test before editing:
| Model | Base/signals | This skill |
|---|---|---|
| Classic V3 widget | Elementor\Widget_Base, Controls_Manager, register_controls(), render() | In scope |
| Atomic / Editor V4 | Modules\AtomicWidgets, Atomic_Widget_Base, prop types, Atomic controls/styles | Out of scope |
| Elementor plugin version | ELEMENTOR_VERSION, currently 4.2.3 in the tested install | Independent of the model name |
Allow both models to coexist in a plugin only behind separate classes and registration paths. Do not make Atomic feature flags a prerequisite for a classic widget.
Workflow
1. Gate the companion plugin before loading widget classes
- Declare
Requires Plugins: elementorin the plugin header on supported WordPress versions. - Run compatibility checks after plugins load. Verify
did_action( 'elementor/loaded' ),ELEMENTOR_VERSION, and the addon's actual PHP minimum. - Do not include a file that extends
Widget_Baseuntil Elementor is loaded; otherwise a missing/inactive Elementor causes a fatal before a notice can run. - Register callbacks only when requirements pass. Keep Pro optional unless the widget genuinely extends a Pro-only API.
Choose and document a real minimum Elementor version. The modern widget registration contract used here is stable since 3.5.0; a tested-up-to value is not a minimum-version claim.
2. Register, do not instantiate early
Hook the manager and pass a widget instance:
add_action(
'elementor/widgets/register',
static function ( \Elementor\Widgets_Manager $widgets_manager ): void {
require_once __DIR__ . '/includes/class-example-widget.php';
$widgets_manager->register( new Example_Widget() );
}
);
- Use
elementor/widgets/register;elementor/widgets/widgets_registeredis deprecated since 3.5.0. - Give
get_name()a stable, globally unique, prefixed lowercase identifier. It becomeswidgetType, is persisted in Elementor JSON, participates in CSS classes, and selects the frontend-ready hook. Renaming it breaks existing content. - Register an optional category on
elementor/elements/categories_registeredwith$elements_manager->add_category(). Keep a fallback category such asgeneral; a category is organization, not authorization. - Never unregister or overwrite another widget merely to resolve a name collision.
3. Implement the smallest correct widget contract
Implement these methods deliberately:
public function get_name(): string;
public function get_title(): string;
public function get_icon(): string;
public function get_categories(): array;
public function get_keywords(): array;
protected function register_controls(): void;
protected function render(): void;
get_icon(), categories, and keywords have base defaults, but explicit metadata makes a public widget discoverable and predictable. Translate human-facing strings; do not translate identifiers, control IDs, script handles, or category slugs.
Use register_controls(), never deprecated _register_controls(). Delegate the control array and value-shape work to elementor-v3-widget-controls.
4. Make PHP rendering canonical and safe
- Read display values with
$this->get_settings_for_display(). It applies active-control conditions and dynamic-tag parsing;get_settings()is raw saved/default data. Process shortcodes only through an explicit renderer such asparse_text_editor()ordo_shortcode()when the widget intentionally supports them. - Validate enumerations again at output time. Saved Elementor JSON, REST/import operations, filters, and dynamic tags can bypass the editor's option list.
- Escape at the final output context:
esc_html(),wp_kses_post(),esc_url(), or an explicitwp_kses()allowlist. Control registration is not an output sanitizer. - Build attributes through
add_render_attribute()andprint_render_attribute_string(). Build URL-control links throughadd_link_attributes(). - Use
add_inline_editing_attributes()only on text nodes intended for editor editing. For repeaters, derive a unique key withget_repeater_setting_key(). - Return early for empty optional content rather than emitting empty semantic elements.
- Emit valid semantic HTML and accessible names/states. Do not use a clickable
divwhere a button or link is required.
Treat render() as the source of truth. Add content_template() only when immediate Backbone-based editor preview is worth maintaining, then keep its structure, conditions, attributes, and escaping intent in parity with PHP. Never move authorization or sensitive lookup logic into the JS template.
Override render_plain_content() when the default rendered HTML is unsuitable for WordPress search, SEO extraction, feeds, or Elementor deactivation. Return meaningful plain content, a shortcode when appropriate, or an empty string for functionality that must not survive deactivation.
5. Register assets once and declare dependencies
Register handles on a WordPress enqueue hook; do not enqueue globally and do not register them on every render() call:
add_action( 'wp_enqueue_scripts', static function (): void {
wp_register_style( 'acme-example-widget', plugins_url( 'assets/widget.css', __FILE__ ), [], '1.0.0' );
wp_register_script( 'acme-example-widget', plugins_url( 'assets/widget.js', __FILE__ ), [ 'elementor-frontend' ], '1.0.0', true );
} );
Return registered handles from get_style_depends() / get_script_depends(). Elementor then loads them for pages containing the widget, including the preview iframe. Use elementor/editor/before_enqueue_scripts or .../after_enqueue_scripts only for code that belongs to the editor panel itself.
For interactive widgets, initialize each instance from:
jQuery( window ).on( 'elementor/frontend/init', () => {
elementorFrontend.hooks.addAction(
'frontend/element_ready/acme-example.default',
( $scope ) => { /* initialize only inside $scope */ }
);
} );
- Make initialization idempotent; editor rerenders can fire the hook repeatedly.
- Scope queries and event teardown to the current
$scope. - Use the exact
get_name()plus.default; skins use their own suffix. - Do not initialize solely on DOM ready: that misses editor rerenders and dynamically inserted elements.
6. Decide output caching from runtime behavior
The base returns true from is_dynamic_content(), so output is not declared cacheable. Override it to false only when output is stable for all users/requests and fully determined by cache-safe settings/dependencies:
protected function is_dynamic_content(): bool {
return false;
}
Keep the default true when rendering depends on the current user, cookies/session, request, time, randomness, stock/entitlement state, uncached remote data, or mutable external state. Elementor separately detects configured dynamic tags, but that does not prove arbitrary PHP logic is static.
For inner-wrapper compatibility and icon rendering, apply elementor-experiments-and-markup. Do not assume .elementor-widget-container exists on core widgets, and render ICONS values through Icons_Manager::render_icon().
Critical rules
- Keep classic V3 and Atomic/V4 classes, controls, styles, and registration paths separate.
- Load a
Widget_Basesubclass only after Elementor is available. - Use the modern manager hook and a stable, prefixed
get_name(). - Treat PHP
render()as canonical and escape every value for its output context. - Register asset handles once; let widget dependency methods control loading.
- Initialize frontend JS through the widget-ready hook and make it idempotent.
- Return
falsefromis_dynamic_content()only after proving cross-user output stability. - Test both editor preview and published frontend; they exercise different render and asset paths.
Review checks
- Bootstrap: inactive/old Elementor produces no fatal and a useful admin state.
- Registration: one unique widget appears exactly once in its expected category.
- Persistence: existing instances survive plugin upgrades because names/control IDs remain stable.
- Rendering: empty, default, rich text, link, media, responsive, repeater, and dynamic-tag states are safe.
- Assets: absent on pages without the widget; present once on pages with one or many instances.
- JS: works after editor rerender and does not duplicate listeners.
- Compatibility: free-only and Pro-active installations; optimized markup on/off where relevant.
- Performance: no unbounded query in registration/render and no false static-cache declaration.
Cross-references
- Run
elementor-v3-widget-controlsfor built-in control schemas, values, selectors, conditions, and repeaters. - Run
elementor-experiments-and-markupwhen rendering icons or depending on wrapper markup. - Run
elementor-deprecationswhile upgrading an older addon or reviewing legacy hooks/methods.
What this skill does NOT cover
- Atomic Widgets / Editor V4 implementation.
- Custom Elementor control classes and control-manager registration.
- Pro-only Forms fields, Theme Builder conditions, nested-element internals, skins, or documents.
- Business-specific authorization, query, REST, or data-storage design beyond the widget boundary.
References
- Detailed bootstrap, widget example, frontend handler, and test matrix:
references/widget-contract-and-example.md. - Official widget documentation: https://developers.elementor.com/docs/widgets/
- Official compatibility checks: https://developers.elementor.com/docs/addons/compatibility/
- Official widget dependencies: https://developers.elementor.com/docs/widgets/widget-dependencies/
- Official output caching: https://developers.elementor.com/docs/widgets/widget-output-caching/
- Verified Elementor Free 4.2.3 source paths:
elementor.phpincludes/managers/widgets.phpincludes/managers/elements.phpincludes/base/widget-base.phpincludes/base/element-base.phpincludes/base/controls-stack.phpincludes/widgets/heading.phpassets/js/frontend.js
- Atomic/V4 boundary verified in
modules/atomic-widgets/andmodules/atomic-widgets/elements/base/atomic-widget-base.php.
Signals
- GitHub stars
- 22
- Forks
- 2
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
elementor-v3-widget-development- Source
- github.com/lonsdale201/wp-agent-skills