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:
- 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_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
$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:
$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:
'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 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
1---2name: elementor-v3-widget-controls3description: 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.4---56# Elementor V3 widget controls78Build 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.910This 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.1112## When to use this skill1314- Add or review `register_controls()` in a `Widget_Base` subclass.15- Choose between regular, responsive, group, repeater, media, URL, or icon controls.16- Use `selectors`, `selectors_dictionary`, `prefix_class`, `condition`, or `conditions`.17- Diagnose a control that saves one shape but `render()` expects another.18- Enable dynamic tags or expose selected settings to widget JavaScript.19- Render repeater rows, responsive values, links, icons, or editor-inline text.20- Audit whether Elementor controls are being mistaken for sanitizers.2122Read `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.2324## Workflow2526### 1. Start from output and data shape2728Before adding panel fields, write down:29301. The semantic output and accessibility behavior.312. The exact saved value shape: scalar, compound array, list, or responsive variants.323. The final output context and validation allowlist.334. Whether a style can be expressed through Elementor selectors or needs PHP/JS.345. Whether the value may use a dynamic tag.3536Do not choose a control by appearance alone. A `URL`, `MEDIA`, `ICONS`, `SLIDER`, `DIMENSIONS`, and `REPEATER` each returns a structured array, not a string.3738### 2. Put controls in explicit sections3940Classic widget controls must be inside a section:4142```php43$this->start_controls_section(44 'section_content',45 [46 'label' => esc_html__( 'Content', 'acme' ),47 'tab' => \Elementor\Controls_Manager::TAB_CONTENT,48 ]49);5051// add_control(), add_responsive_control(), add_group_control()...5253$this->end_controls_section();54```5556- Use stable, prefixed IDs when collision or future injection is plausible.57- Use `TAB_CONTENT` for data/behavior and `TAB_STYLE` for presentation.58- Do not nest sections; `Controls_Stack` rejects controls outside a section and section misuse can terminate panel construction.59- Keep editor labels/descriptions translated and concise. Never translate IDs, option keys, CSS selectors, or stored values.60- Use headings, separators, popovers, and tabs only to clarify a real grouping.6162### 3. Choose the smallest built-in control that matches the value6364| Need | Control | Render-time shape/check |65|---|---|---|66| Short plain input | `TEXT`, `NUMBER`, `TEXTAREA` | scalar; validate/escape for use |67| Restricted choice | `SELECT`, `CHOOSE`, `SWITCHER` | scalar; re-check against allowlist |68| Rich content | `WYSIWYG` | string; use an explicit HTML policy |69| Link | `URL` | `url`, `is_external`, `nofollow`, `custom_attributes` |70| Image/file | `MEDIA` | `id`, `url`, `size`; prefer attachment APIs when ID exists |71| Icon | `ICONS` | `value`, `library`; render via `Icons_Manager` |72| Size | `SLIDER` | `size`, `unit`, optionally `sizes` |73| Box values | `DIMENSIONS` | `top/right/bottom/left/unit/isLinked` |74| Multiple images | `GALLERY` | list of attachment-like arrays |75| Repeated rows | `REPEATER` | list of row maps, each with stable `_id` |7677Use `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.7879### 4. Use display settings for rendering8081```php82$settings = $this->get_settings_for_display();83```8485This 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.8687Control definitions do **not** establish a security boundary:8889- A `SELECT` option list does not prevent an imported/filtered/database value outside the list.90- A numeric UI range does not prove the saved value is in range.91- Dynamic tags can replace a value at display time.92- A conditional hidden control may still exist in raw document data; the display value can be `null` when inactive.9394Validate 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.9596### 5. Let selectors handle deterministic styles9798Use `selectors` for styles fully derived from a control:99100```php101$this->add_responsive_control(102 'gap',103 [104 'label' => esc_html__( 'Gap', 'acme' ),105 'type' => \Elementor\Controls_Manager::SLIDER,106 'size_units' => [ 'px', 'em', 'rem' ],107 'range' => [ 'px' => [ 'min' => 0, 'max' => 100 ] ],108 'selectors' => [109 '{{WRAPPER}} .acme-card__list' => 'gap: {{SIZE}}{{UNIT}};',110 ],111 ]112);113```114115- Anchor selectors at `{{WRAPPER}}` to prevent cross-widget leakage.116- Use `{{VALUE}}`, `{{SIZE}}`, and `{{UNIT}}` only where the chosen control supplies them.117- Use `selectors_dictionary` to map stored choices to CSS values instead of embedding arbitrary CSS.118- Use `{{CURRENT_ITEM}}` for per-row repeater styling.119- Prefer `add_responsive_control()` only when per-device values make sense; do not manually guess generated breakpoint suffixes.120- Use `prefix_class` only with a tight option allowlist and a namespaced prefix.121122Selector 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.123124### 6. Use conditions as editor UX, not runtime authorization125126Simple equality/membership belongs in `condition`; compound logic belongs in `conditions`:127128```php129'condition' => [ 'show_icon' => 'yes' ],130131'conditions' => [132 'relation' => 'or',133 'terms' => [134 [ 'name' => 'columns', 'operator' => '>', 'value' => 1 ],135 [ 'name' => 'columns', 'operator' => '===', 'value' => '' ],136 ],137],138```139140Use 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.141142### 7. Prefer group controls for coherent CSS features143144Use `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`.145146Do 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.147148### 8. Render repeaters with stable keys149150Create fields with `new \Elementor\Repeater()` and pass `$repeater->get_controls()` to a `REPEATER` control. `get_fields()` is deprecated.151152At render time:1531541. Confirm the setting is an array.1552. Validate each row field independently.1563. Build a unique attribute/link key per row with `get_repeater_setting_key()` or a namespaced index key.1574. Use the row `_id`/`{{CURRENT_ITEM}}` contract for row-specific styling; do not use array order as a persistent identity.1585. Bound any query or remote work driven by rows; avoid N+1 lookups.159160For 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.161162### 9. Expose only intentional frontend settings163164`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.165166## Critical rules167168- Keep classic control arrays out of Atomic/V4 classes.169- Put widget controls inside balanced sections; do not nest sections.170- Match the render code to the control's actual scalar/compound/list value shape.171- Use `get_settings_for_display()` for normal rendering and handle inactive `null` values.172- Treat every setting as untrusted at output, including select values and dynamic tags.173- Anchor style selectors at `{{WRAPPER}}` and whitelist class/tag/CSS choices.174- Treat conditions as panel UX, never authorization.175- Use `get_controls()` for repeater fields and stable per-row render keys.176- Render URL, media, and icon values through their dedicated APIs.177- Keep large datasets asynchronous or use a bounded manual-ID fallback.178179## Review checks180181- Every control is in the right tab/section and has a stable unique ID.182- Defaults match the control's real value shape and render assumptions.183- Responsive settings are not read as one unsuffixed scalar in custom PHP/JS logic.184- Selector placeholders match the control shape and remain wrapper-scoped.185- Conditions reference controls at a supported scope and inactive values are handled.186- Dynamic-tag eligibility matches the semantic value type.187- Output validation/escaping exists independently of the editor UI.188- Repeaters have bounded work, stable keys, safe empty state, and no N+1 query.189- `frontend_available` reveals no sensitive data.190- 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.191192## Cross-references193194- Run **`elementor-v3-widget-development`** for addon bootstrap, widget registration, rendering, assets, JS lifecycle, and caching.195- Run **`elementor-dynamic-tag-ajax-select`** for large dataset selectors and Pro/free degradation.196- Run **`elementor-experiments-and-markup`** for `ICONS` output and optimized wrapper behavior.197198## What this skill does NOT cover199200- Creating or registering a custom Elementor control class.201- Atomic Widgets / Editor V4 prop types, controls, or style schema.202- Pro Forms fields, nested elements, skins, documents, or Theme Builder controls.203- Generic WordPress form processing, persistence, REST authorization, or business rules.204205## References206207- Built-in control catalog, value shapes, group controls, selectors, repeater pattern, and escaping matrix: `references/built-in-controls-and-patterns.md`.208- Official editor controls documentation: <https://developers.elementor.com/docs/editor-controls/>209- Official conditional display documentation: <https://developers.elementor.com/docs/editor-controls/conditional-display/>210- Official repeater control documentation: <https://developers.elementor.com/docs/editor-controls/control-repeater/>211- Official widget rendering documentation: <https://developers.elementor.com/docs/widgets/>212- Verified Elementor Free 4.2.3 source paths:213 - `includes/managers/controls.php`214 - `includes/base/controls-stack.php`215 - `includes/controls/`216 - `includes/controls/groups/`217 - `includes/elements/repeater.php`218 - `includes/base/element-base.php`219 - `includes/base/widget-base.php`220 - `includes/widgets/heading.php`221 - `includes/widgets/icon-list.php`