{"slug":"elementor-v3-widget-development","title":"elementor-v3-widget-development","summary":"Builds 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, a","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-16T14:51:43.564705Z","repo":{"url":"https://github.com/Lonsdale201/wp-agent-skills","stars":22,"forks":2,"license":"MIT","updatedAt":"2026-09-26T23:03:36Z"},"bodyHtml":"<hr>\n<h2>name: elementor-v3-widget-development\ndescription: &gt;-\nBuilds and reviews production-ready classic Elementor widgets based on\n<code>Elementor\\Widget_Base</code>: companion-plugin bootstrap and compatibility gates,\n<code>elementor/widgets/register</code>, widget identity, PHP and editor rendering,\nrender attributes, asset dependencies, frontend handlers, accessibility,\noutput caching, and regression tests. Use when code extends <code>Widget_Base</code>,\nimplements <code>register_controls()</code> or <code>render()</code>, registers an Elementor\nwidget/category, or must distinguish the established V3 widget API from\nAtomic Widgets / Editor V4. Does not cover custom control types.\nmetadata:\nwp-skills-author: \"Soczó Kristóf\"\nwp-skills-contact: \"mailto:lonsdale201@hotmail.com\"\nwp-skills-plugin: \"elementor\"\nwp-skills-plugin-version-tested: \"4.2.3 (free) / 4.2.2 (pro)\"\nwp-skills-wp-version-tested: \"7.1\"\nwp-skills-php-min: \"7.4\"\nwp-skills-api-stable-since: \"3.5.0\"\nwp-skills-last-updated: \"2026-08-22\"</h2>\n<h1>Elementor V3 widget development</h1>\n<p>Build companion-plugin widgets on Elementor's established <code>Widget_Base</code> / <code>Controls_Stack</code> architecture. Treat <strong>V3</strong> 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.</p>\n<p>Do not mix this model with Atomic Widgets / Editor V4. Atomic widgets extend classes under <code>Elementor\\Modules\\AtomicWidgets</code>, declare prop types and styles differently, and remain a moving surface. Never translate a V3 control array into an Atomic schema by guesswork.</p>\n<h2>When to use this skill</h2>\n<ul>\n<li>Create or review a class extending <code>\\Elementor\\Widget_Base</code>.</li>\n<li>Register widgets, categories, scripts, or styles from an Elementor addon.</li>\n<li>Implement <code>register_controls()</code>, <code>render()</code>, <code>content_template()</code>, or <code>render_plain_content()</code>.</li>\n<li>Add widget frontend JavaScript through <code>frontend/element_ready/{widget-name}.default</code>.</li>\n<li>Decide whether <code>is_dynamic_content()</code> may return <code>false</code>.</li>\n<li>Diagnose a widget visible in PHP but missing/broken in the editor or frontend.</li>\n<li>Migrate <code>_register_controls()</code> or <code>elementor/widgets/widgets_registered</code> to current APIs.</li>\n</ul>\n<p>For a complete companion-plugin skeleton and test matrix, read <code>references/widget-contract-and-example.md</code>. Load <strong><code>elementor-v3-widget-controls</code></strong> as well when designing or reviewing the control schema.</p>\n<h2>Architecture boundary</h2>\n<p>Use the following identity test before editing:</p>\n<table>\n<thead>\n<tr>\n<th>Model</th>\n<th>Base/signals</th>\n<th>This skill</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Classic V3 widget</td>\n<td><code>Elementor\\Widget_Base</code>, <code>Controls_Manager</code>, <code>register_controls()</code>, <code>render()</code></td>\n<td>In scope</td>\n</tr>\n<tr>\n<td>Atomic / Editor V4</td>\n<td><code>Modules\\AtomicWidgets</code>, <code>Atomic_Widget_Base</code>, prop types, Atomic controls/styles</td>\n<td>Out of scope</td>\n</tr>\n<tr>\n<td>Elementor plugin version</td>\n<td><code>ELEMENTOR_VERSION</code>, currently 4.2.3 in the tested install</td>\n<td>Independent of the model name</td>\n</tr>\n</tbody>\n</table>\n<p>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.</p>\n<h2>Workflow</h2>\n<h3>1. Gate the companion plugin before loading widget classes</h3>\n<ol>\n<li>Declare <code>Requires Plugins: elementor</code> in the plugin header on supported WordPress versions.</li>\n<li>Run compatibility checks after plugins load. Verify <code>did_action( 'elementor/loaded' )</code>, <code>ELEMENTOR_VERSION</code>, and the addon's actual PHP minimum.</li>\n<li>Do not include a file that extends <code>Widget_Base</code> until Elementor is loaded; otherwise a missing/inactive Elementor causes a fatal before a notice can run.</li>\n<li>Register callbacks only when requirements pass. Keep Pro optional unless the widget genuinely extends a Pro-only API.</li>\n</ol>\n<p>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.</p>\n<h3>2. Register, do not instantiate early</h3>\n<p>Hook the manager and pass a widget instance:</p>\n<pre><code>add_action(\n    'elementor/widgets/register',\n    static function ( \\Elementor\\Widgets_Manager $widgets_manager ): void {\n        require_once __DIR__ . '/includes/class-example-widget.php';\n        $widgets_manager-&gt;register( new Example_Widget() );\n    }\n);\n</code></pre>\n<ul>\n<li>Use <code>elementor/widgets/register</code>; <code>elementor/widgets/widgets_registered</code> is deprecated since 3.5.0.</li>\n<li>Give <code>get_name()</code> a stable, globally unique, prefixed lowercase identifier. It becomes <code>widgetType</code>, is persisted in Elementor JSON, participates in CSS classes, and selects the frontend-ready hook. Renaming it breaks existing content.</li>\n<li>Register an optional category on <code>elementor/elements/categories_registered</code> with <code>$elements_manager-&gt;add_category()</code>. Keep a fallback category such as <code>general</code>; a category is organization, not authorization.</li>\n<li>Never unregister or overwrite another widget merely to resolve a name collision.</li>\n</ul>\n<h3>3. Implement the smallest correct widget contract</h3>\n<p>Implement these methods deliberately:</p>\n<pre><code>public function get_name(): string;\npublic function get_title(): string;\npublic function get_icon(): string;\npublic function get_categories(): array;\npublic function get_keywords(): array;\nprotected function register_controls(): void;\nprotected function render(): void;\n</code></pre>\n<p><code>get_icon()</code>, 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.</p>\n<p>Use <code>register_controls()</code>, never deprecated <code>_register_controls()</code>. Delegate the control array and value-shape work to <strong><code>elementor-v3-widget-controls</code></strong>.</p>\n<h3>4. Make PHP rendering canonical and safe</h3>\n<ol>\n<li>Read display values with <code>$this-&gt;get_settings_for_display()</code>. It applies active-control conditions and dynamic-tag parsing; <code>get_settings()</code> is raw saved/default data. Process shortcodes only through an explicit renderer such as <code>parse_text_editor()</code> or <code>do_shortcode()</code> when the widget intentionally supports them.</li>\n<li>Validate enumerations again at output time. Saved Elementor JSON, REST/import operations, filters, and dynamic tags can bypass the editor's option list.</li>\n<li>Escape at the final output context: <code>esc_html()</code>, <code>wp_kses_post()</code>, <code>esc_url()</code>, or an explicit <code>wp_kses()</code> allowlist. Control registration is not an output sanitizer.</li>\n<li>Build attributes through <code>add_render_attribute()</code> and <code>print_render_attribute_string()</code>. Build URL-control links through <code>add_link_attributes()</code>.</li>\n<li>Use <code>add_inline_editing_attributes()</code> only on text nodes intended for editor editing. For repeaters, derive a unique key with <code>get_repeater_setting_key()</code>.</li>\n<li>Return early for empty optional content rather than emitting empty semantic elements.</li>\n<li>Emit valid semantic HTML and accessible names/states. Do not use a clickable <code>div</code> where a button or link is required.</li>\n</ol>\n<p>Treat <code>render()</code> as the source of truth. Add <code>content_template()</code> 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.</p>\n<p>Override <code>render_plain_content()</code> 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.</p>\n<h3>5. Register assets once and declare dependencies</h3>\n<p>Register handles on a WordPress enqueue hook; do not enqueue globally and do not register them on every <code>render()</code> call:</p>\n<pre><code>add_action( 'wp_enqueue_scripts', static function (): void {\n    wp_register_style( 'acme-example-widget', plugins_url( 'assets/widget.css', __FILE__ ), [], '1.0.0' );\n    wp_register_script( 'acme-example-widget', plugins_url( 'assets/widget.js', __FILE__ ), [ 'elementor-frontend' ], '1.0.0', true );\n} );\n</code></pre>\n<p>Return registered handles from <code>get_style_depends()</code> / <code>get_script_depends()</code>. Elementor then loads them for pages containing the widget, including the preview iframe. Use <code>elementor/editor/before_enqueue_scripts</code> or <code>.../after_enqueue_scripts</code> only for code that belongs to the editor panel itself.</p>\n<p>For interactive widgets, initialize each instance from:</p>\n<pre><code>jQuery( window ).on( 'elementor/frontend/init', () =&gt; {\n    elementorFrontend.hooks.addAction(\n        'frontend/element_ready/acme-example.default',\n        ( $scope ) =&gt; { /* initialize only inside $scope */ }\n    );\n} );\n</code></pre>\n<ul>\n<li>Make initialization idempotent; editor rerenders can fire the hook repeatedly.</li>\n<li>Scope queries and event teardown to the current <code>$scope</code>.</li>\n<li>Use the exact <code>get_name()</code> plus <code>.default</code>; skins use their own suffix.</li>\n<li>Do not initialize solely on DOM ready: that misses editor rerenders and dynamically inserted elements.</li>\n</ul>\n<h3>6. Decide output caching from runtime behavior</h3>\n<p>The base returns <code>true</code> from <code>is_dynamic_content()</code>, so output is not declared cacheable. Override it to <code>false</code> <strong>only</strong> when output is stable for all users/requests and fully determined by cache-safe settings/dependencies:</p>\n<pre><code>protected function is_dynamic_content(): bool {\n    return false;\n}\n</code></pre>\n<p>Keep the default <code>true</code> 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.</p>\n<p>For inner-wrapper compatibility and icon rendering, apply <strong><code>elementor-experiments-and-markup</code></strong>. Do not assume <code>.elementor-widget-container</code> exists on core widgets, and render <code>ICONS</code> values through <code>Icons_Manager::render_icon()</code>.</p>\n<h2>Critical rules</h2>\n<ul>\n<li>Keep classic V3 and Atomic/V4 classes, controls, styles, and registration paths separate.</li>\n<li>Load a <code>Widget_Base</code> subclass only after Elementor is available.</li>\n<li>Use the modern manager hook and a stable, prefixed <code>get_name()</code>.</li>\n<li>Treat PHP <code>render()</code> as canonical and escape every value for its output context.</li>\n<li>Register asset handles once; let widget dependency methods control loading.</li>\n<li>Initialize frontend JS through the widget-ready hook and make it idempotent.</li>\n<li>Return <code>false</code> from <code>is_dynamic_content()</code> only after proving cross-user output stability.</li>\n<li>Test both editor preview and published frontend; they exercise different render and asset paths.</li>\n</ul>\n<h2>Review checks</h2>\n<ul>\n<li>Bootstrap: inactive/old Elementor produces no fatal and a useful admin state.</li>\n<li>Registration: one unique widget appears exactly once in its expected category.</li>\n<li>Persistence: existing instances survive plugin upgrades because names/control IDs remain stable.</li>\n<li>Rendering: empty, default, rich text, link, media, responsive, repeater, and dynamic-tag states are safe.</li>\n<li>Assets: absent on pages without the widget; present once on pages with one or many instances.</li>\n<li>JS: works after editor rerender and does not duplicate listeners.</li>\n<li>Compatibility: free-only and Pro-active installations; optimized markup on/off where relevant.</li>\n<li>Performance: no unbounded query in registration/render and no false static-cache declaration.</li>\n</ul>\n<h2>Cross-references</h2>\n<ul>\n<li>Run <strong><code>elementor-v3-widget-controls</code></strong> for built-in control schemas, values, selectors, conditions, and repeaters.</li>\n<li>Run <strong><code>elementor-experiments-and-markup</code></strong> when rendering icons or depending on wrapper markup.</li>\n<li>Run <strong><code>elementor-deprecations</code></strong> while upgrading an older addon or reviewing legacy hooks/methods.</li>\n</ul>\n<h2>What this skill does NOT cover</h2>\n<ul>\n<li>Atomic Widgets / Editor V4 implementation.</li>\n<li>Custom Elementor control classes and control-manager registration.</li>\n<li>Pro-only Forms fields, Theme Builder conditions, nested-element internals, skins, or documents.</li>\n<li>Business-specific authorization, query, REST, or data-storage design beyond the widget boundary.</li>\n</ul>\n<h2>References</h2>\n<ul>\n<li>Detailed bootstrap, widget example, frontend handler, and test matrix: <code>references/widget-contract-and-example.md</code>.</li>\n<li>Official widget documentation: <a href=\"https://developers.elementor.com/docs/widgets/\">https://developers.elementor.com/docs/widgets/</a></li>\n<li>Official compatibility checks: <a href=\"https://developers.elementor.com/docs/addons/compatibility/\">https://developers.elementor.com/docs/addons/compatibility/</a></li>\n<li>Official widget dependencies: <a href=\"https://developers.elementor.com/docs/widgets/widget-dependencies/\">https://developers.elementor.com/docs/widgets/widget-dependencies/</a></li>\n<li>Official output caching: <a href=\"https://developers.elementor.com/docs/widgets/widget-output-caching/\">https://developers.elementor.com/docs/widgets/widget-output-caching/</a></li>\n<li>Verified Elementor Free 4.2.3 source paths:\n<ul>\n<li><code>elementor.php</code></li>\n<li><code>includes/managers/widgets.php</code></li>\n<li><code>includes/managers/elements.php</code></li>\n<li><code>includes/base/widget-base.php</code></li>\n<li><code>includes/base/element-base.php</code></li>\n<li><code>includes/base/controls-stack.php</code></li>\n<li><code>includes/widgets/heading.php</code></li>\n<li><code>assets/js/frontend.js</code></li>\n</ul>\n</li>\n<li>Atomic/V4 boundary verified in <code>modules/atomic-widgets/</code> and <code>modules/atomic-widgets/elements/base/atomic-widget-base.php</code>.</li>\n</ul>\n","files":[{"path":"agents/openai.yaml","sizeBytes":250,"isText":true},{"path":"references/widget-contract-and-example.md","sizeBytes":15561,"isText":true},{"path":"SKILL.md","sizeBytes":12728,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-16T14:55:17.93738Z","sha256":"FB2E1674E947C546A297323EA88BD06E2B27C20A27CCF94E198E9E477407165C","sizeBytes":10535},"review":null,"source":{"repositoryUrl":"https://github.com/Lonsdale201/wp-agent-skills","path":"elementor/elementor-v3-widget-development","license":"MIT","commit":"c51b571a259f0c4b5f5c0a3bc50ed580c6851f98","subtreeSha":"954651120286E2D98E775AEF39D15E4F77DF52151D90B5B832294B54C7CF4770","lastSyncedAt":"2026-09-29T23:33:03.303675Z"},"reviewedAt":"2026-09-16T15:10:08.689261Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/elementor/elementor-v3-widget-development"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart"},{"target":"git","command":"git clone https://github.com/Lonsdale201/wp-agent-skills.git"}]}