elementor-v3-widget-controls
Designs and reviews built-in controls for classic Elementor `Widget_Base` widgets: content/style sections, control value shapes, responsive and group controls, CSS selectors, conditions, dynamic tags, URL/media/icons values, repeaters, inline editing, and safe PHP rendering. Use
Install
npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/elementor/elementor-v3-widget-controls
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
git clone https://github.com/Lonsdale201/wp-agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Elementor V3 widget controls
Build editor panels from Elementor's built-in classic controls and couple each saved value to safe, predictable rendering. “V3” means the established Widget_Base / Controls_Stack model even when the installed plugin is Elementor 4.x. Do not apply these arrays to Atomic Widgets / Editor V4.
This skill does not create custom control types. Prefer a built-in control or a well-defined fallback; a custom-control integration is a separate lifecycle, asset, and compatibility problem.
When to use this skill
- Add or review
register_controls()in aWidget_Basesubclass. - Choose between regular, responsive, group, repeater, media, URL, or icon controls.
- Use
selectors,selectors_dictionary,prefix_class,condition, orconditions. - Diagnose a control that saves one shape but
render()expects another. - Enable dynamic tags or expose selected settings to widget JavaScript.
- Render repeater rows, responsive values, links, icons, or editor-inline text.
- Audit whether Elementor controls are being mistaken for sanitizers.
Read references/built-in-controls-and-patterns.md when implementing value shapes, selector tokens, group controls, repeaters, or a full example. Pair this skill with elementor-v3-widget-development for bootstrap, registration, assets, caching, and frontend lifecycle.
Workflow
1. Start from output and data shape
Before adding panel fields, write down:
- The semantic output and accessibility behavior.
- The exact saved value shape: scalar, compound array, list, or responsive variants.
- The final output context and validation allowlist.
- Whether a style can be expressed through Elementor selectors or needs PHP/JS.
- Whether the value may use a dynamic tag.
Do not choose a control by appearance alone. A URL, MEDIA, ICONS, SLIDER, DIMENSIONS, and REPEATER each returns a structured array, not a string.
2. Put controls in explicit sections
Classic widget controls must be inside a section:
$this->start_controls_section(
'section_content',
[
'label' => esc_html__( 'Content', 'acme' ),
'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
]
);
// add_control(), add_responsive_control(), add_group_control()...
$this->end_controls_section();
- Use stable, prefixed IDs when collision or future injection is plausible.
- Use
TAB_CONTENTfor data/behavior andTAB_STYLEfor presentation. - Do not nest sections;
Controls_Stackrejects controls outside a section and section misuse can terminate panel construction. - Keep editor labels/descriptions translated and concise. Never translate IDs, option keys, CSS selectors, or stored values.
- Use headings, separators, popovers, and tabs only to clarify a real grouping.
3. Choose the smallest built-in control that matches the value
| Need | Control | Render-time shape/check |
|---|---|---|
| Short plain input | TEXT, NUMBER, TEXTAREA |
scalar; validate/escape for use |
| Restricted choice | SELECT, CHOOSE, SWITCHER |
scalar; re-check against allowlist |
| Rich content | WYSIWYG |
string; use an explicit HTML policy |
| Link | URL |
url, is_external, nofollow, custom_attributes |
| Image/file | MEDIA |
id, url, size; prefer attachment APIs when ID exists |
| Icon | ICONS |
value, library; render via Icons_Manager |
| Size | SLIDER |
size, unit, optionally sizes |
| Box values | DIMENSIONS |
top/right/bottom/left/unit/isLinked |
| Multiple images | GALLERY |
list of attachment-like arrays |
| Repeated rows | REPEATER |
list of row maps, each with stable _id |
Use RAW_HTML, HEADING, DIVIDER, and POPOVER_TOGGLE as panel UI, not as content storage. Do not put secrets or authorization state in any control: Elementor document settings are content data, not a confidential store.
4. Use display settings for rendering
$settings = $this->get_settings_for_display();
This returns active settings after conditions and dynamic-tag parsing. It does not grant permission to run arbitrary shortcodes; process shortcodes only through an explicit, intentional renderer. Use raw $this->get_settings() only for a specifically documented need such as inspecting stored configuration before dynamic resolution.
Control definitions do not establish a security boundary:
- A
SELECToption list does not prevent an imported/filtered/database value outside the list. - A numeric UI range does not prove the saved value is in range.
- Dynamic tags can replace a value at display time.
- A conditional hidden control may still exist in raw document data; the display value can be
nullwhen inactive.
Validate allowed HTML tags, element names, CSS classes, IDs, numbers, URLs, attachment visibility, and business permissions in the code that consumes them. Escape at final output.
5. Let selectors handle deterministic styles
Use selectors for styles fully derived from a control:
$this->add_responsive_control(
'gap',
[
'label' => esc_html__( 'Gap', 'acme' ),
'type' => \Elementor\Controls_Manager::SLIDER,
'size_units' => [ 'px', 'em', 'rem' ],
'range' => [ 'px' => [ 'min' => 0, 'max' => 100 ] ],
'selectors' => [
'{{WRAPPER}} .acme-card__list' => 'gap: {{SIZE}}{{UNIT}};',
],
]
);
- Anchor selectors at
{{WRAPPER}}to prevent cross-widget leakage. - Use
{{VALUE}},{{SIZE}}, and{{UNIT}}only where the chosen control supplies them. - Use
selectors_dictionaryto map stored choices to CSS values instead of embedding arbitrary CSS. - Use
{{CURRENT_ITEM}}for per-row repeater styling. - Prefer
add_responsive_control()only when per-device values make sense; do not manually guess generated breakpoint suffixes. - Use
prefix_classonly with a tight option allowlist and a namespaced prefix.
Selector output is presentation, not permission enforcement or server-side validation. Do not interpolate arbitrary editor text into property names, selectors, at-rules, or unrestricted declarations.
6. Use conditions as editor UX, not runtime authorization
Simple equality/membership belongs in condition; compound logic belongs in conditions:
'condition' => [ 'show_icon' => 'yes' ],
'conditions' => [
'relation' => 'or',
'terms' => [
[ 'name' => 'columns', 'operator' => '>', 'value' => 1 ],
[ 'name' => 'columns', 'operator' => '===', 'value' => '' ],
],
],
Use supported operators only. Conditions change panel visibility and active settings; they do not authorize output or delete stored values. Inside a repeater, an inner field may depend on another field in the same row. Do not make an inner field depend on an outer/main control; Elementor documents that cross-level dependency as unsupported.
7. Prefer group controls for coherent CSS features
Use add_group_control() with official types such as Typography, Background, Border, Box Shadow, Text Shadow, Text Stroke, CSS Filter, or Image Size. Give each group a unique name and its target selector.
Do not manually recreate the group's internal control IDs or read guessed keys. Let the group generate selectors, or use its documented renderer/helper (for example image-size output) where required.
8. Render repeaters with stable keys
Create fields with new \Elementor\Repeater() and pass $repeater->get_controls() to a REPEATER control. get_fields() is deprecated.
At render time:
- Confirm the setting is an array.
- Validate each row field independently.
- Build a unique attribute/link key per row with
get_repeater_setting_key()or a namespaced index key. - Use the row
_id/{{CURRENT_ITEM}}contract for row-specific styling; do not use array order as a persistent identity. - Bound any query or remote work driven by rows; avoid N+1 lookups.
For large remote/post/product/user datasets, do not preload thousands of SELECT2 options. Apply elementor-dynamic-tag-ajax-select for the Pro AJAX Query Control plus a free-safe manual-ID fallback.
9. Expose only intentional frontend settings
frontend_available => true makes a control available to frontend handlers; it is not a secure transport. Expose only values required by JS, never secrets, nonces intended for another action, capability decisions, private IDs, or raw privileged data. Re-authorize every server request made by the handler.
Critical rules
- Keep classic control arrays out of Atomic/V4 classes.
- Put widget controls inside balanced sections; do not nest sections.
- Match the render code to the control's actual scalar/compound/list value shape.
- Use
get_settings_for_display()for normal rendering and handle inactivenullvalues. - Treat every setting as untrusted at output, including select values and dynamic tags.
- Anchor style selectors at
{{WRAPPER}}and whitelist class/tag/CSS choices. - Treat conditions as panel UX, never authorization.
- Use
get_controls()for repeater fields and stable per-row render keys. - Render URL, media, and icon values through their dedicated APIs.
- Keep large datasets asynchronous or use a bounded manual-ID fallback.
Review checks
- Every control is in the right tab/section and has a stable unique ID.
- Defaults match the control's real value shape and render assumptions.
- Responsive settings are not read as one unsuffixed scalar in custom PHP/JS logic.
- Selector placeholders match the control shape and remain wrapper-scoped.
- Conditions reference controls at a supported scope and inactive values are handled.
- Dynamic-tag eligibility matches the semantic value type.
- Output validation/escaping exists independently of the editor UI.
- Repeaters have bounded work, stable keys, safe empty state, and no N+1 query.
frontend_availablereveals no sensitive data.- Style-control tests account for Elementor's optimized split stack; query a known control by ID instead of treating a context-dependent bulk
get_controls()list as complete.
Cross-references
- Run
elementor-v3-widget-developmentfor addon bootstrap, widget registration, rendering, assets, JS lifecycle, and caching. - Run
elementor-dynamic-tag-ajax-selectfor large dataset selectors and Pro/free degradation. - Run
elementor-experiments-and-markupforICONSoutput and optimized wrapper behavior.
What this skill does NOT cover
- Creating or registering a custom Elementor control class.
- Atomic Widgets / Editor V4 prop types, controls, or style schema.
- Pro Forms fields, nested elements, skins, documents, or Theme Builder controls.
- Generic WordPress form processing, persistence, REST authorization, or business rules.
References
- Built-in control catalog, value shapes, group controls, selectors, repeater pattern, and escaping matrix:
references/built-in-controls-and-patterns.md. - Official editor controls documentation: https://developers.elementor.com/docs/editor-controls/
- Official conditional display documentation: https://developers.elementor.com/docs/editor-controls/conditional-display/
- Official repeater control documentation: https://developers.elementor.com/docs/editor-controls/control-repeater/
- Official widget rendering documentation: https://developers.elementor.com/docs/widgets/
- Verified Elementor Free 4.2.3 source paths:
includes/managers/controls.phpincludes/base/controls-stack.phpincludes/controls/includes/controls/groups/includes/elements/repeater.phpincludes/base/element-base.phpincludes/base/widget-base.phpincludes/widgets/heading.phpincludes/widgets/icon-list.php
Files (wp-agent-skills)
-
agents
-
openai.yaml 256 B
interface: display_name: "Elementor V3 Widget Controls" short_description: "Design safe built-in controls for classic widgets." default_prompt: "Use $elementor-v3-widget-controls to design or review built-in controls for a classic Elementor widget."
-
-
references
-
built-in-controls-and-patterns.md 13.8 KB
# Built-in classic controls and rendering patterns Read this reference when the implementation depends on a control's returned value shape, responsive CSS, conditions, dynamic tags, group controls, or repeaters. All classes listed here are available in Elementor Free 4.2.3 unless explicitly noted. ## Control catalog `Elementor\Controls_Manager` exposes these classic control constants in the tested source: | Family | Constants | Typical value | |---|---|---| | Plain data | `TEXT`, `NUMBER`, `TEXTAREA`, `WYSIWYG`, `CODE`, `HIDDEN` | scalar string/number-like value | | Choice | `SELECT`, `SELECT2`, `CHOOSE`, `SWITCHER` | scalar; `SELECT2` can return a list when `multiple` is true | | Panel-only UI | `HEADING`, `RAW_HTML`, `DIVIDER`, `POPOVER_TOGGLE` | layout/instruction rather than business content | | Style/data | `COLOR`, `SLIDER`, `DIMENSIONS`, `TEXT_SHADOW`, `DATE_TIME` | scalar or compound shape | | Media | `MEDIA`, `GALLERY`, `ICONS` | attachment/icon arrays | | Collection | `REPEATER` | list of row maps | The source also registers internal section/tab/WP-widget controls. Do not use internal manager controls merely because their constants or classes are discoverable. ## Value-shape table ### Scalars | Control | Common returned value | Required consumer check | |---|---|---| | `TEXT`, `TEXTAREA`, `WYSIWYG`, `CODE`, `HIDDEN` | string | Cast defensively; escape or KSES at output | | `NUMBER` | number-like scalar | `is_numeric()`, cast, clamp to the business range | | `SELECT`, `CHOOSE` | selected option key | Strict allowlist; never trust the editor options alone | | `SWITCHER` | configured `return_value`, normally `'yes'`, or empty | Compare strictly to the declared return value | | `COLOR` | CSS color string | Prefer a `selectors` declaration; validate if used in custom output | | `DATE_TIME` | date/time string | Parse against the expected format/timezone before business use | `SELECT2` returns a scalar in single mode and an array in multiple mode. Normalize explicitly rather than accepting both accidentally. ### Compound values ```php // URL [ 'url' => '', 'is_external' => '', 'nofollow' => '', 'custom_attributes' => '', ] // MEDIA [ 'id' => '', 'url' => '', 'size' => '', ] // ICONS [ 'value' => '', 'library' => '', ] // SLIDER [ 'size' => '', 'unit' => 'px', 'sizes' => [], ] // DIMENSIONS [ 'top' => '', 'right' => '', 'bottom' => '', 'left' => '', 'unit' => 'px', 'isLinked' => true, ] ``` `GALLERY` is a list of attachment-like arrays. `REPEATER` is a list of row maps and Elementor adds a stable `_id` to each row. Defaults must use the same shape. A common broken URL default is `'default' => 'https://example.com'`; the correct form is: ```php 'default' => [ 'url' => 'https://example.com/', 'is_external' => true, 'nofollow' => true, ], ``` ## Dedicated renderers ### URL ```php if ( ! empty( $settings['link']['url'] ) ) { $this->add_link_attributes( 'cta', $settings['link'] ); ?> <a <?php $this->print_render_attribute_string( 'cta' ); ?>> <?php echo esc_html( $settings['label'] ); ?> </a> <?php } ``` `add_link_attributes()` handles URL escaping, target, nofollow, and Elementor's parsed custom-attribute format. In 4.2.3, custom attributes cannot override `href` and `on*` event attributes are rejected. Still do not interpret URL settings as proof that the destination is authorized or public. ### Media Prefer WordPress attachment rendering when an ID exists: ```php $image_id = absint( $settings['image']['id'] ?? 0 ); if ( $image_id ) { echo wp_get_attachment_image( $image_id, 'large', false, [ 'class' => 'acme-card__image', ] ); } elseif ( ! empty( $settings['image']['url'] ) ) { ?> <img class="acme-card__image" src="<?php echo esc_url( $settings['image']['url'] ); ?>" alt=""> <?php } ``` Use `Group_Control_Image_Size::get_attachment_image_html()` when the editor must choose registered/custom image sizes. Do not expose a private attachment merely because an editor saved its ID. ### Icons ```php if ( ! empty( $settings['icon']['value'] ) ) { \Elementor\Icons_Manager::render_icon( $settings['icon'], [ 'aria-hidden' => 'true' ] ); } ``` Use an accessible text label for an action or remove `aria-hidden` and provide a meaningful icon label only when the icon itself conveys unique content. Never hardcode a Font Awesome `i` element; Inline Font Icons can replace font icons with SVG and omit font CSS on the published frontend. ## Dynamic tags Enable compatible dynamic content deliberately: ```php $this->add_control( 'title', [ 'label' => esc_html__( 'Title', 'acme' ), 'type' => \Elementor\Controls_Manager::TEXT, 'dynamic' => [ 'active' => true ], ] ); ``` Some control classes declare a default dynamic category/property (URL, media, slider, and others). Do not guess a category constant or force a type-incompatible tag. Review the concrete control class and the dynamic tag's declared category. Render with `get_settings_for_display()`. Elementor then resolves configured tags and filters inactive settings. Escape the resolved result; the tag's source does not make the result safe for every HTML context. ## Conditions ### Simple conditions ```php 'condition' => [ 'layout' => [ 'stacked', 'inline' ], 'icon!' => '', ], ``` The `!` suffix expresses inequality in the simple condition syntax. Use arrays for accepted values. Keep simple conditions readable; use `conditions` for explicit operators. ### Advanced conditions ```php 'conditions' => [ 'relation' => 'and', 'terms' => [ [ 'name' => 'enabled', 'operator' => '===', 'value' => 'yes' ], [ 'name' => 'count', 'operator' => '>=', 'value' => 2 ], ], ], ``` Documented operators are `==`, `!=`, `!==`, `in`, `!in`, `contains`, `!contains`, `<`, `<=`, `>`, `>=`, and `===`. The default relation and operator are `and` and `===`. Condition scope matters: - A top-level control may depend on another top-level control. - A repeater inner field may depend on another field in the same row. - A repeater inner field cannot depend on a main/top-level control. - Hiding does not erase the stored value or authorize the corresponding output. ## Selectors and responsive controls ### Selector placeholders | Placeholder | Use | |---|---| | `{{WRAPPER}}` | The current widget wrapper; always scope custom widget CSS here | | `{{VALUE}}` | Scalar control value or mapped dictionary value | | `{{SIZE}}` / `{{UNIT}}` | Unit-control members such as Slider/Dimensions | | `{{CURRENT_ITEM}}` | Current repeater row's generated class | Use a dictionary to map stored semantic keys to CSS: ```php $this->add_control( 'alignment', [ 'label' => esc_html__( 'Alignment', 'acme' ), 'type' => \Elementor\Controls_Manager::CHOOSE, 'options' => [ 'start' => [ 'title' => esc_html__( 'Start', 'acme' ), 'icon' => 'eicon-text-align-left' ], 'center' => [ 'title' => esc_html__( 'Center', 'acme' ), 'icon' => 'eicon-text-align-center' ], 'end' => [ 'title' => esc_html__( 'End', 'acme' ), 'icon' => 'eicon-text-align-right' ], ], 'default' => 'start', 'selectors_dictionary' => [ 'start' => 'start', 'center' => 'center', 'end' => 'end', ], 'selectors' => [ '{{WRAPPER}} .acme-card' => 'text-align: {{VALUE}};', ], ] ); ``` Use `add_responsive_control()` for CSS that Elementor can emit at its configured breakpoints. Avoid using a responsive control to choose server-rendered semantic markup: PHP renders one response, while device variants are represented in generated CSS/settings. Do not construct internal `_tablet`/`_mobile` keys manually. ## Group controls The tested Free source includes these common group control families: - Typography - Background - Border - Box Shadow - Text Shadow - Text Stroke - CSS Filter - Image Size Use the class's `get_type()` and a unique name: ```php $this->add_group_control( \Elementor\Group_Control_Border::get_type(), [ 'name' => 'card_border', 'selector' => '{{WRAPPER}} .acme-card', ] ); ``` A group creates multiple internal setting keys. Do not read guessed keys such as `card_border_width` unless the public group contract explicitly requires it. Prefer its generated selectors/helper. ## Repeater pattern ```php $repeater = new \Elementor\Repeater(); $repeater->add_control( 'text', [ 'label' => esc_html__( 'Text', 'acme' ), 'type' => \Elementor\Controls_Manager::TEXT, 'default' => esc_html__( 'List item', 'acme' ), ] ); $repeater->add_control( 'link', [ 'label' => esc_html__( 'Link', 'acme' ), 'type' => \Elementor\Controls_Manager::URL, ] ); $repeater->add_control( 'color', [ 'label' => esc_html__( 'Color', 'acme' ), 'type' => \Elementor\Controls_Manager::COLOR, 'selectors' => [ '{{WRAPPER}} {{CURRENT_ITEM}} .acme-list__text' => 'color: {{VALUE}};', ], ] ); $this->add_control( 'items', [ 'label' => esc_html__( 'Items', 'acme' ), 'type' => \Elementor\Controls_Manager::REPEATER, 'fields' => $repeater->get_controls(), 'title_field' => '{{{ text }}}', 'default' => [ [ 'text' => esc_html__( 'First item', 'acme' ) ], ], ] ); ``` Safe PHP rendering: ```php $items = is_array( $settings['items'] ?? null ) ? $settings['items'] : []; if ( $items ) { echo '<ul class="acme-list">'; foreach ( $items as $index => $item ) { $text = trim( (string) ( $item['text'] ?? '' ) ); if ( '' === $text ) { continue; } $text_key = $this->get_repeater_setting_key( 'text', 'items', $index ); $link_key = 'item_link_' . $index; $this->add_render_attribute( $text_key, 'class', 'acme-list__text' ); $this->add_inline_editing_attributes( $text_key, 'none' ); echo '<li class="elementor-repeater-item-' . esc_attr( $item['_id'] ?? '' ) . '">'; if ( ! empty( $item['link']['url'] ) ) { $this->add_link_attributes( $link_key, $item['link'] ); echo '<a ' . $this->get_render_attribute_string( $link_key ) . '>'; } echo '<span ' . $this->get_render_attribute_string( $text_key ) . '>' . esc_html( $text ) . '</span>'; if ( ! empty( $item['link']['url'] ) ) { echo '</a>'; } echo '</li>'; } echo '</ul>'; } ``` The concatenated attribute strings above are already escaped by Elementor's attribute renderer; annotate that fact for PHPCS rather than escaping the whole attribute string again. If direct echo makes review harder, switch to PHP template blocks and `print_render_attribute_string()`. Do not perform a database or HTTP lookup inside each row. Gather all IDs first, validate/bound the count, fetch in one operation, then map results back by stable ID. ## Output policy matrix | Setting use | Minimum handling | |---|---| | Plain visible text | `esc_html()` | | Attribute value | `add_render_attribute()` or `esc_attr()` | | URL control | `add_link_attributes()`; otherwise `esc_url()` | | Limited rich text | `wp_kses()` with component allowlist | | General post-like rich text | `wp_kses_post()` if that broad policy is intended | | HTML tag name | strict allowlist or `Elementor\Utils::validate_html_tag()` | | CSS class suffix | strict allowlist plus namespaced prefix | | Number | numeric check, cast, min/max clamp | | Attachment | `absint()`, visibility/permission rule, attachment renderer | | Icon | `Icons_Manager::render_icon()` | Never use `print_unescaped_setting()` for ordinary addon output. It is intentionally unescaped and shifts the full safety proof onto the caller. ## Elementor 4.x optimized control-stack caveat Do not use a bulk frontend/CLI control listing as proof that a style control failed to register. With control optimization active, `Controls_Manager::add_control_to_stack()` stores controls recognized as style controls in a separate `style_controls` stack. `Controls_Stack::get_controls()` merges that stack only when `Performance::is_use_style_controls()` is true for the current context. The single-control lookup deliberately checks both stacks: ```php $gap = $widget->get_controls( 'item_gap' ); if ( ! $gap ) { throw new RuntimeException( 'The expected style control is missing.' ); } ``` Therefore: - Use `get_controls( 'known-id' )` for a registration assertion. - Inspect the editor configuration when testing panel visibility. - Do not assert that `array_keys( $widget->get_controls() )` is a complete schema in every frontend/CLI context. - Do not build production behavior by inventorying internal control stacks; consume documented settings and widget APIs. This split is an optimization detail, not a second control-registration API. Continue to call `add_control()`, `add_responsive_control()`, and `add_group_control()` normally. ## Grounding notes - Control constants and registrations: `includes/managers/controls.php`. - Value shapes: `includes/controls/url.php`, `media.php`, `icons.php`, `slider.php`, `dimensions.php`, and `gallery.php`. - Sections, selectors, responsive/group methods, display settings, render attributes, and the optimized split-stack lookup: `includes/base/controls-stack.php`. - Style-control stack classification: `includes/managers/controls.php`. - URL attributes: `includes/base/element-base.php` and `includes/utils.php::parse_custom_attributes()`. - Repeater `_id`, `get_controls()`, and defaults: `includes/elements/repeater.php` and `includes/controls/repeater.php`. - Native models: `includes/widgets/heading.php` for a compact widget and `includes/widgets/icon-list.php` for repeater rendering.
-
-
SKILL.md 12.4 KB
--- name: elementor-v3-widget-controls description: >- Designs and reviews built-in controls for classic Elementor `Widget_Base` widgets: content/style sections, control value shapes, responsive and group controls, CSS selectors, conditions, dynamic tags, URL/media/icons values, repeaters, inline editing, and safe PHP rendering. Use when code calls `start_controls_section()`, `add_control()`, `add_responsive_control()`, `add_group_control()`, creates `Repeater`, uses `selectors` or `condition`, or reads `get_settings_for_display()`. Excludes custom control classes and Atomic/V4 controls. metadata: wp-skills-author: "Soczó Kristóf" wp-skills-contact: "mailto:lonsdale201@hotmail.com" wp-skills-plugin: "elementor" wp-skills-plugin-version-tested: "4.2.3 (free) / 4.2.2 (pro)" wp-skills-wp-version-tested: "7.1" wp-skills-php-min: "7.4" wp-skills-api-stable-since: "3.5.0" wp-skills-last-updated: "2026-08-22" --- # Elementor V3 widget controls Build editor panels from Elementor's **built-in classic controls** and couple each saved value to safe, predictable rendering. “V3” means the established `Widget_Base` / `Controls_Stack` model even when the installed plugin is Elementor 4.x. Do not apply these arrays to Atomic Widgets / Editor V4. This skill does not create custom control types. Prefer a built-in control or a well-defined fallback; a custom-control integration is a separate lifecycle, asset, and compatibility problem. ## When to use this skill - Add or review `register_controls()` in a `Widget_Base` subclass. - Choose between regular, responsive, group, repeater, media, URL, or icon controls. - Use `selectors`, `selectors_dictionary`, `prefix_class`, `condition`, or `conditions`. - Diagnose a control that saves one shape but `render()` expects another. - Enable dynamic tags or expose selected settings to widget JavaScript. - Render repeater rows, responsive values, links, icons, or editor-inline text. - Audit whether Elementor controls are being mistaken for sanitizers. Read `references/built-in-controls-and-patterns.md` when implementing value shapes, selector tokens, group controls, repeaters, or a full example. Pair this skill with **`elementor-v3-widget-development`** for bootstrap, registration, assets, caching, and frontend lifecycle. ## Workflow ### 1. Start from output and data shape Before adding panel fields, write down: 1. The semantic output and accessibility behavior. 2. The exact saved value shape: scalar, compound array, list, or responsive variants. 3. The final output context and validation allowlist. 4. Whether a style can be expressed through Elementor selectors or needs PHP/JS. 5. Whether the value may use a dynamic tag. Do not choose a control by appearance alone. A `URL`, `MEDIA`, `ICONS`, `SLIDER`, `DIMENSIONS`, and `REPEATER` each returns a structured array, not a string. ### 2. Put controls in explicit sections Classic widget controls must be inside a section: ```php $this->start_controls_section( 'section_content', [ 'label' => esc_html__( 'Content', 'acme' ), 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, ] ); // add_control(), add_responsive_control(), add_group_control()... $this->end_controls_section(); ``` - Use stable, prefixed IDs when collision or future injection is plausible. - Use `TAB_CONTENT` for data/behavior and `TAB_STYLE` for presentation. - Do not nest sections; `Controls_Stack` rejects controls outside a section and section misuse can terminate panel construction. - Keep editor labels/descriptions translated and concise. Never translate IDs, option keys, CSS selectors, or stored values. - Use headings, separators, popovers, and tabs only to clarify a real grouping. ### 3. Choose the smallest built-in control that matches the value | Need | Control | Render-time shape/check | |---|---|---| | Short plain input | `TEXT`, `NUMBER`, `TEXTAREA` | scalar; validate/escape for use | | Restricted choice | `SELECT`, `CHOOSE`, `SWITCHER` | scalar; re-check against allowlist | | Rich content | `WYSIWYG` | string; use an explicit HTML policy | | Link | `URL` | `url`, `is_external`, `nofollow`, `custom_attributes` | | Image/file | `MEDIA` | `id`, `url`, `size`; prefer attachment APIs when ID exists | | Icon | `ICONS` | `value`, `library`; render via `Icons_Manager` | | Size | `SLIDER` | `size`, `unit`, optionally `sizes` | | Box values | `DIMENSIONS` | `top/right/bottom/left/unit/isLinked` | | Multiple images | `GALLERY` | list of attachment-like arrays | | Repeated rows | `REPEATER` | list of row maps, each with stable `_id` | Use `RAW_HTML`, `HEADING`, `DIVIDER`, and `POPOVER_TOGGLE` as panel UI, not as content storage. Do not put secrets or authorization state in any control: Elementor document settings are content data, not a confidential store. ### 4. Use display settings for rendering ```php $settings = $this->get_settings_for_display(); ``` This returns active settings after conditions and dynamic-tag parsing. It does not grant permission to run arbitrary shortcodes; process shortcodes only through an explicit, intentional renderer. Use raw `$this->get_settings()` only for a specifically documented need such as inspecting stored configuration before dynamic resolution. Control definitions do **not** establish a security boundary: - A `SELECT` option list does not prevent an imported/filtered/database value outside the list. - A numeric UI range does not prove the saved value is in range. - Dynamic tags can replace a value at display time. - A conditional hidden control may still exist in raw document data; the display value can be `null` when inactive. Validate allowed HTML tags, element names, CSS classes, IDs, numbers, URLs, attachment visibility, and business permissions in the code that consumes them. Escape at final output. ### 5. Let selectors handle deterministic styles Use `selectors` for styles fully derived from a control: ```php $this->add_responsive_control( 'gap', [ 'label' => esc_html__( 'Gap', 'acme' ), 'type' => \Elementor\Controls_Manager::SLIDER, 'size_units' => [ 'px', 'em', 'rem' ], 'range' => [ 'px' => [ 'min' => 0, 'max' => 100 ] ], 'selectors' => [ '{{WRAPPER}} .acme-card__list' => 'gap: {{SIZE}}{{UNIT}};', ], ] ); ``` - Anchor selectors at `{{WRAPPER}}` to prevent cross-widget leakage. - Use `{{VALUE}}`, `{{SIZE}}`, and `{{UNIT}}` only where the chosen control supplies them. - Use `selectors_dictionary` to map stored choices to CSS values instead of embedding arbitrary CSS. - Use `{{CURRENT_ITEM}}` for per-row repeater styling. - Prefer `add_responsive_control()` only when per-device values make sense; do not manually guess generated breakpoint suffixes. - Use `prefix_class` only with a tight option allowlist and a namespaced prefix. Selector output is presentation, not permission enforcement or server-side validation. Do not interpolate arbitrary editor text into property names, selectors, at-rules, or unrestricted declarations. ### 6. Use conditions as editor UX, not runtime authorization Simple equality/membership belongs in `condition`; compound logic belongs in `conditions`: ```php 'condition' => [ 'show_icon' => 'yes' ], 'conditions' => [ 'relation' => 'or', 'terms' => [ [ 'name' => 'columns', 'operator' => '>', 'value' => 1 ], [ 'name' => 'columns', 'operator' => '===', 'value' => '' ], ], ], ``` Use supported operators only. Conditions change panel visibility and active settings; they do not authorize output or delete stored values. Inside a repeater, an inner field may depend on another field in the same row. Do not make an inner field depend on an outer/main control; Elementor documents that cross-level dependency as unsupported. ### 7. Prefer group controls for coherent CSS features Use `add_group_control()` with official types such as Typography, Background, Border, Box Shadow, Text Shadow, Text Stroke, CSS Filter, or Image Size. Give each group a unique `name` and its target `selector`. Do not manually recreate the group's internal control IDs or read guessed keys. Let the group generate selectors, or use its documented renderer/helper (for example image-size output) where required. ### 8. Render repeaters with stable keys Create fields with `new \Elementor\Repeater()` and pass `$repeater->get_controls()` to a `REPEATER` control. `get_fields()` is deprecated. At render time: 1. Confirm the setting is an array. 2. Validate each row field independently. 3. Build a unique attribute/link key per row with `get_repeater_setting_key()` or a namespaced index key. 4. Use the row `_id`/`{{CURRENT_ITEM}}` contract for row-specific styling; do not use array order as a persistent identity. 5. Bound any query or remote work driven by rows; avoid N+1 lookups. For large remote/post/product/user datasets, do not preload thousands of `SELECT2` options. Apply **`elementor-dynamic-tag-ajax-select`** for the Pro AJAX Query Control plus a free-safe manual-ID fallback. ### 9. Expose only intentional frontend settings `frontend_available => true` makes a control available to frontend handlers; it is not a secure transport. Expose only values required by JS, never secrets, nonces intended for another action, capability decisions, private IDs, or raw privileged data. Re-authorize every server request made by the handler. ## Critical rules - Keep classic control arrays out of Atomic/V4 classes. - Put widget controls inside balanced sections; do not nest sections. - Match the render code to the control's actual scalar/compound/list value shape. - Use `get_settings_for_display()` for normal rendering and handle inactive `null` values. - Treat every setting as untrusted at output, including select values and dynamic tags. - Anchor style selectors at `{{WRAPPER}}` and whitelist class/tag/CSS choices. - Treat conditions as panel UX, never authorization. - Use `get_controls()` for repeater fields and stable per-row render keys. - Render URL, media, and icon values through their dedicated APIs. - Keep large datasets asynchronous or use a bounded manual-ID fallback. ## Review checks - Every control is in the right tab/section and has a stable unique ID. - Defaults match the control's real value shape and render assumptions. - Responsive settings are not read as one unsuffixed scalar in custom PHP/JS logic. - Selector placeholders match the control shape and remain wrapper-scoped. - Conditions reference controls at a supported scope and inactive values are handled. - Dynamic-tag eligibility matches the semantic value type. - Output validation/escaping exists independently of the editor UI. - Repeaters have bounded work, stable keys, safe empty state, and no N+1 query. - `frontend_available` reveals no sensitive data. - Style-control tests account for Elementor's optimized split stack; query a known control by ID instead of treating a context-dependent bulk `get_controls()` list as complete. ## Cross-references - Run **`elementor-v3-widget-development`** for addon bootstrap, widget registration, rendering, assets, JS lifecycle, and caching. - Run **`elementor-dynamic-tag-ajax-select`** for large dataset selectors and Pro/free degradation. - Run **`elementor-experiments-and-markup`** for `ICONS` output and optimized wrapper behavior. ## What this skill does NOT cover - Creating or registering a custom Elementor control class. - Atomic Widgets / Editor V4 prop types, controls, or style schema. - Pro Forms fields, nested elements, skins, documents, or Theme Builder controls. - Generic WordPress form processing, persistence, REST authorization, or business rules. ## References - Built-in control catalog, value shapes, group controls, selectors, repeater pattern, and escaping matrix: `references/built-in-controls-and-patterns.md`. - Official editor controls documentation: <https://developers.elementor.com/docs/editor-controls/> - Official conditional display documentation: <https://developers.elementor.com/docs/editor-controls/conditional-display/> - Official repeater control documentation: <https://developers.elementor.com/docs/editor-controls/control-repeater/> - Official widget rendering documentation: <https://developers.elementor.com/docs/widgets/> - Verified Elementor Free 4.2.3 source paths: - `includes/managers/controls.php` - `includes/base/controls-stack.php` - `includes/controls/` - `includes/controls/groups/` - `includes/elements/repeater.php` - `includes/base/element-base.php` - `includes/base/widget-base.php` - `includes/widgets/heading.php` - `includes/widgets/icon-list.php`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.