WordPress & Elementor Pro — Skill Router
A complete procedural guide for producing production-grade WordPress and Elementor Pro
code. This file is the router — read it first, then load the relevant sub-file(s)
for the task at hand.
Sub-file Map — Read the Right File for Each Task
| Task type |
Read this file |
| Plugin scaffold, child theme, code placement, CPT, AJAX handler |
scaffolding.md |
| PHP standards, sanitization, escaping, nonces, WP_Error, transients, password hashing |
php-standards.md |
| JavaScript standards, enqueue API, defer/async, wp_add_inline_script |
js-css-standards.md |
| CSS standards, BEM, design tokens, Elementor CSS selectors |
js-css-standards.md |
| Elementor custom widget, Dynamic Tags, Loop Grid, Form actions, Theme Builder conditions |
elementor-patterns.md |
| Elementor extension points — custom form fields, theme locations, injecting controls into native widgets, adding a TAB to the editor's Elements panel, Finder/context-menu, hooks reference, deprecations |
elementor-extending.md |
| WooCommerce HPOS, order API, template overrides, Loop Grid for products |
woocommerce.md |
| REST API endpoints, schema, permission callbacks |
rest-api.md |
| Off-canvas UI, off-canvas accessibility, focus trap |
offcanvas-ui.md |
| Performance checklists (frontend + backend), speculative loading, IE conditional comments |
performance.md |
| Accessibility checklist, WCAG 2.2 AA, ARIA patterns |
performance.md |
Hard-won production gotchas — widget lifecycle fatals, content_template() escaping, CSS-in-Elementor footguns, transactional email, wp.org review/packaging, embedding apps, AJAX/loop re-rendering, dynamic CSS in loops, Swiper on optimised sites |
field-notes.md |
| wordpress.org submission — the 18 Directory Guidelines, Plugin Check 2.1.0 categories/usage, review process, required headers/readme |
wp-org-guidelines.md |
| Subversion (SVN) — deploying/tagging a release on the wordpress.org plugin/theme SVN (trunk/tags/assets), the daily work cycle, branching/merging, properties, repo admin |
svn/svn.md (self-contained sub-bundle; svn/references/ goes deeper) |
Debugging & static analysis — PHPCS+WPCS, PHPStan, Plugin Check, WP_DEBUG/Query Monitor, Elementor Safe Mode/cache, symptom→cause table |
debugging.md |
Common WordPress APIs — admin settings page (Settings + Options API), register_meta, roles/capabilities, WP-Cron, internationalization (i18n) |
wordpress-apis.md |
Database layer (MariaDB/MySQL) — custom tables + dbDelta(), charsets & the varchar(191) index rule, indexes, EXPLAIN, MariaDB↔MySQL portability, sql_mode, full-text |
mariadb.md |
Widget Boilerplates — Load when building a widget of that type
| Widget type |
Read this file |
| Button / CTA with icon and link |
widget-button.md |
| Container / layout wrapper / section / card shell |
widget-container.md |
| Image with caption, link, lightbox |
widget-image.md |
| Heading / title / HTML tag selector |
widget-heading.md |
| Rich text / WYSIWYG body content |
widget-text-editor.md |
| Video embed (YouTube, Vimeo, self-hosted) |
widget-video.md |
| Widget that renders a saved Elementor template by ID |
widget-elementor-template.md (SELECT2, get_builder_content_for_display(), Dynamic Tags context, CSS timing) |
| Widget whose markup lives in a separate PHP template file |
widget-php-template.md (Strategies A/B/C, load_template(), ob_start, path-traversal safety) |
| Divider / horizontal rule with optional text or icon |
widget-divider.md |
| Spacer / vertical gap |
widget-spacer.md |
| Single standalone icon with optional link |
widget-icon.md |
| Icon + title + description box |
widget-icon-box.md |
| Image + title + description box |
widget-image-box.md |
| Image grid / gallery |
widget-image-gallery.md |
| Image slider / carousel |
widget-image-carousel.md |
| Bullet list with icons per item |
widget-icon-list.md |
| Animated number counter |
widget-counter.md |
| Percentage progress bar |
widget-progress.md |
| Customer quote / testimonial |
widget-testimonial.md |
| Tabbed content panels |
widget-tabs.md |
| Accordion — one panel open at a time |
widget-accordion.md |
| Toggle — multiple panels open simultaneously |
widget-toggle.md |
| Social media icon links row |
widget-social-icons.md |
| Colored alert / notice box |
widget-alert.md |
| Audio player (SoundCloud / self-hosted) |
widget-audio.md |
| WordPress shortcode output |
widget-shortcode.md |
| Raw custom HTML / JS / CSS embed |
widget-html.md |
| Named anchor for in-page navigation |
widget-menu-anchor.md |
| WordPress registered sidebar output |
widget-sidebar.md |
| WordPress read more tag |
widget-read-more.md |
| Google Maps embed |
widget-google-maps.md |
| Decorative star rating display |
widget-star-rating.md |
| Schema-ready structured rating |
widget-rating.md |
| Text following a curved / custom SVG path |
widget-text-path.md |
| Nested Tabs / Nested Accordion (each panel is a Container holding any widgets) |
widget-nested.md |
Always read the relevant sub-file before writing code. For tasks that span multiple
areas (e.g. a WooCommerce widget with custom REST endpoint), read all relevant sub-files.
For a widget task, read BOTH the widget boilerplate file AND elementor-patterns.md.
For any custom widget, plugin, or wp.org-bound work, also skim field-notes.md — it
catches the lint-passing, review-failing, site-down mistakes the topic files don't dwell on.
0. Golden Rules (Never Violate)
These override everything in all sub-files:
- Native APIs first — WordPress core hook before plugin; Elementor API before template override.
- Sanitize in, escape out — Every input sanitized. Every output escaped. No exceptions.
- Prefix everything — All functions, classes, constants, hooks, and CSS classes use a project-specific prefix.
- State your placement — Every code response must declare exactly where the code lives.
- No over-clarifying — Only ask a clarification question if the missing info would materially change the code output. Otherwise, state your assumption and proceed.
- NEVER hardcode visual settings in widgets — Every visual property (colors, fonts, sizes,
spacing, backgrounds, borders, shadows, alignment) MUST be exposed as a standard Elementor
control in the editor panel. Users control appearance via the toolbar — not by editing code.
See §5 "Mandatory Widget Controls" below for the required controls checklist.
- Name for the directory from day one — Naming is decided first and is effectively
irreversible, so get it right before writing code. The plugin slug/name must NOT start with
a trademark you don't own —
"CRM for WooCommerce", never "WooCommerce CRM" (Directory
Guideline 17). The text domain must exactly equal the plugin slug. The wp.org slug is
permanent, and a widget's get_name() is sticky (stored in every page's
_elementor_data — renaming it breaks placed widgets). Choose the public name, slug, text
domain, and code prefix (Rule #3) together, once. This is distinct from Rule #3: that
governs internal code symbols; this governs the public identity. See wp-org-guidelines.md
(Guidelines 12/16/17) and field-notes.md §1 (sticky get_name()).
1. Default Assumptions
Quickly assess — only ask if the answer would change the code:
| Info needed |
Ask only if... |
| Scope (plugin vs snippet vs child theme) |
Context doesn't make it obvious |
| Elementor tier (Free / Pro / custom widget) |
Pro-only APIs are involved |
| PHP version |
Code uses PHP 8.3+ features like typed class constants, or 8.4+ features like property hooks |
| WooCommerce / ACF / WPML present |
Integration with those systems is required |
Default stack when not stated (full release-by-release history lives in CHANGELOG.md — keep volatile version-tracking out of this router):
| Component |
Version |
Notes |
| WordPress |
7.1 |
"Mary Lou", released Aug 19, 2026 (7.0 "Armstrong" was May 20; the 7.0 branch ended at 7.0.4, Aug 12). Minimum PHP 7.4 (7.2/7.3 dropped — sites still on them stay pinned to 6.9.x). No multisite assumed. 7.1 makes the post editor iframe unconditional — see below. |
| PHP |
8.3 recommended |
7.4 = minimum. 8.4 / 8.5 = "beta support" (possible deprecation notices). 8.2 fully compatible but no longer the recommended default. |
| Elementor (free + Pro) |
4.2+ |
Separate plugins with independent version numbers — currently free 4.2.3 / Pro 4.2.2 (both Aug 19, 2026). 4.0.0 (Mar 30, 2026) made the Atomic Editor stable + default for new installs; 4.2.0 (Jul 20) added Atomic Grid (free) and Atomic Loop (Pro). ⚠️ Elementor's header still reads "Tested up to: 7.0.4" — it has not declared WP 7.1 support. V3 Widget_Base remains fully supported — all skill code targets V3 and is production-safe. |
| WooCommerce |
11.0+ |
11.0 (Aug 4, 2026) REMOVED the product editor beta and bumped to Action Scheduler 4.0.0 (breaking dedup change). HPOS default-on since 8.2; 10.7 disabled HPOS "sync on read"; 10.9 defers Store API draft-order creation — see woocommerce.md. |
Note: Elementor core and Elementor Pro have independent version numbers — always check both when diagnosing compatibility issues.
WordPress 7.1 — what changed for plugin / Elementor devs
WP 7.1 "Mary Lou" shipped August 19, 2026 (WordCamp US). Unlike 7.0, this release
contains four changes that can break a shipped plugin — they are listed first. No PHP or
WordPress minimum changed.
🔴 Breaking — audit your plugin for these:
- The post editor is now ALWAYS in an iframe — including on sites registering legacy meta
boxes, which was the last remaining escape hatch. Editor JavaScript that reaches for the
global
document / window now targets the wrong document. Use the canvas element's
ownerDocument and defaultView instead:// ❌ breaks in 7.1 — this is the OUTER document, not the editor canvas
document.querySelector( '.my-block' ).classList.add( 'ready' );
// ✅ resolve the document from a node you already own inside the canvas
const doc = myCanvasNode.ownerDocument;
const view = doc.defaultView; // the iframe's `window`
doc.querySelector( '.my-block' )?.classList.add( 'ready' );
view.requestAnimationFrame( … );
Injected <style>/<link> must go into the canvas document too, or it styles nothing.
Elementor's own editor is unaffected (it is not the block editor) — this hits block
registrations, meta boxes, and any block-editor integration your plugin ships.
__next40pxDefaultSize is now a no-op on @wordpress/components — remove the prop
entirely; there is no replacement. Form controls render at 40px unconditionally.
- List-table markup changed: the row header moved from the checkbox column to the title
column. Any CSS/JS selector keyed on
th.check-column (or assuming the row header's position)
breaks — audit custom admin list tables and column callbacks.
- jQuery UI updated to 1.14.2. Test anything depending on jQuery UI behaviour or styling.
🟢 New and useful:
- SVG Icon API (public in 7.1) — register icons and whole collections, render server-side,
read over REST:
wp_register_icon_collection(), wp_register_icon(), wp_get_icon().
⚠️ SVGs pass through a conservative allowlist — only <svg>, <path> and <polygon> survive.
A <g>, <circle>, <rect> or <use> in your icon is stripped silently, so convert shapes to
paths before registering. (This is a core API — Elementor widgets still ship their own SVGs;
see elementor-extending.md §8 and the "don't wp_kses() inline SVG" rule in field-notes.md §6.)
- Abilities API matured:
wp_get_abilities() filtering, execution-lifecycle hooks, custom
validation, a unified public exposure flag, and client-compatible JSON-Schema preparation.
Registration timing rules from 7.0 are unchanged (see the 7.0 section below).
- Global Styles /
theme.json: responsive style variations with configurable breakpoints,
pseudo-state styling (:hover / :focus / :active), and text-shadow support.
- Media: client-side image processing in the browser via WebAssembly (compression/resizing
no longer hit the server), infinite scroll in the Media Library, and registration of multiple
image sizes at once. Multisite now enforces upload limits on media sideloading.
- Persistent admin bar across editor screens — review any custom toolbar nodes for behaviour
in the editor context.
- Design System: semantic design tokens for theming admin UI (relevant if you build admin or
editor panels — compare the Elementor panel tokens in
elementor-extending.md §5).
- DataViews / DataForm APIs matured for data-driven admin interfaces.
- React stays on 18.3 — React 19 was deferred again. Don't bundle
react/jsx-runtime
yourself, and avoid string refs / defaultProps, which fail under the experimental React 19 flag.
- Conditional block-CSS loading can affect remote content pulls — verify if you render blocks
outside a normal page request.
Sources: wordpress.org/news/2026/08/mary-lou/ · make.wordpress.org/core/2026/08/05/wordpress-7-1-field-guide/ ·
developer.wordpress.org/news/2026/08/whats-new-for-developers-august-2026/
WordPress 7.0 — what changed for plugin / Elementor devs
WP 7.0 "Armstrong" shipped May 20, 2026 (delayed from the original April 9 target while
the RTC storage layer was redesigned — see below). Everything below is opt-in and
non-breaking; most plugin/Elementor work is unaffected. It remains relevant because 7.1
changed none of it.
- Minimum PHP is now 7.4 (7.2/7.3 dropped). The skill's recommended baseline stays
PHP 8.3. Bump your plugin's
Requires PHP header to 7.4 only once you target WP 7.0+
exclusively. No new DB minimum is enforced; wordpress.org/about/requirements/ recommends
MariaDB 10.11+ or MySQL 8.0+ (the MariaDB floor was raised from 10.6; re-verified Aug 2026).
- Real-Time Collaboration (RTC): simultaneous multi-author block editing (CRDT-based, via
an HTTP-polling sync provider — not WebRTC). Data is stored in a dedicated core database
table; an earlier
wp_post_meta / wp_sync_storage design was rejected, and building the
table is what pushed the release from April to May. Plugin impact: scope every
WP_Query / get_posts() with an explicit post_type so internal core post types never
leak into your results — do not hardcode any internal RTC type name. The
WP_ALLOW_COLLABORATION constant lets hosts swap the sync transport.
- WP AI Client: provider-agnostic PHP + JS AI API —
wp_ai_client_prompt( $prompt )->generate_text().
Guard with function_exists( 'wp_ai_client_prompt' ).
- Abilities API:
wp_register_ability() (PHP, since WP 6.9) plus a JS counterpart in 7.0.
Use 'meta' => ['show_in_rest' => true] to expose via REST. Register ONLY inside the
wp_abilities_api_init hook (core renamed the feature plugin's abilities_api_init;
categories go on wp_abilities_api_categories_init) or you get _doing_it_wrong; a
category is required and must already be registered. Calling it also sets your real
Requires at least floor to 6.9 — Plugin Check errors on the header and
function_exists() guards don't count (see debugging.md §1).
- Connectors UI (Settings → Connectors) for managing AI provider credentials, and a
Command Palette in wp-admin.
- Iframed editor was still conditional in 7.0 — 7.1 makes it unconditional. See the 7.1
section above;
"apiVersion": 3 in block.json is now table stakes, not preparation.
Sources: make.wordpress.org/core/2026/01/09/dropping-support-for-php-7-2-and-7-3/ ·
make.wordpress.org/core/2026/04/22/wordpress-7-0-release-party-updated-schedule/ ·
wordpress.org/about/requirements/
📌 PHP support labels (unchanged in WP 7.1): PHP 7.4–8.3 fully compatible; 8.3 recommended;
8.4 (WP 6.7+) and 8.5 (WP 6.9+) carry a "beta support" label (possible deprecation notices).
Source: make.wordpress.org/core/handbook/references/php-compatibility-and-wordpress-versions/
✅ Elementor 4.x status (current: free 4.2.3 / Pro 4.2.2, both Aug 19, 2026): Elementor
4.0.0 (Mar 30, 2026, free + Pro) made the Atomic Editor stable and the default for new
installs and added Atomic Forms, Pro Interactions, and Component creation. Updating to 4.x
leaves existing sites untouched — V3 widgets and V4 Atomic Elements coexist on the same
page; Atomic features are toggled at WP Admin → Elementor → Editor → Settings.
There is still NO third-party API for building Atomic Elements, and Elementor has said so
explicitly — they will not release one soon and advise against integrating with Atomic
internals until they announce it (elementor/elementor GitHub Discussion #32950). Atomic
Elements are documented only as a data structure
(developers.elementor.com/docs/data-structure/atomic-elements). So continue using V3
Widget_Base for all third-party widgets — it is the correct, production-safe API and all
skill code targets it. (Re-verified Aug 23, 2026 — unchanged since the July check.)
⚠️ Elementor has not declared WP 7.1 compatibility yet. As of free 4.2.3 / Pro 4.2.2 the
Tested up to header reads 7.0.4, three days after WP 7.1 shipped. That is normal lag,
not a known incompatibility — but when a client reports editor breakage on a fresh 7.1 site,
check Elementor's current Tested up to before debugging your own code.
V4 Atomic Elements that now ship by default (awareness only — not third-party-buildable yet):
Div Block & Flexbox Container (layout); Atomic Grid — advanced row/column layouts (free
4.2.0, Jul 20, 2026); Atomic Heading, Paragraph, Image, Button, Video, SVG;
Atomic Tabs; Atomic Forms (Pro) with composable fields — Label, Input, Textarea,
Checkbox, Submit, plus Radio, Select, Date Picker, Time Picker, and File Upload (added in
Pro 4.1.0, May 26, 2026); and Atomic Loop — dynamic content layouts, the V4 counterpart
of the Pro Loop Grid (Pro 4.2.0, Jul 20, 2026). These are end-user elements; building
custom atomic elements still awaits the finalized V4 extension docs — keep targeting V3
Widget_Base until then.
Source: elementor.com/products/website-builder/v4-faq/ ·
developers.elementor.com/elementor-editor-4-0-developers-update/ ·
elementor.com/pro/changelog/ · github.com/elementor/elementor/releases
🗓️ Release-by-release history (betas, RCs, point releases) lives in CHANGELOG.md.
Keep this router focused on durable guidance; update version facts in the table above
and in CHANGELOG.md, not scattered across the sub-files.
2. Architecture Decision Tree
Run through this mentally before writing a single line:
Does a WordPress core hook (add_action/add_filter) solve it?
YES → Use the hook. No plugin needed. Place in child theme functions.php
or Elementor Custom Code.
NO → Does Elementor's PHP/JS API solve it?
YES → Extend via Elementor hooks, Dynamic Tags, or Widget_Base.
Register via elementor/widgets/register or elementor/dynamic_tags/register.
NO → Is this logic reusable across themes or sites?
YES → Scaffold a dedicated plugin (see scaffolding.md).
NO → Child theme functions.php or Elementor Custom Code block.
Is a WooCommerce override needed?
→ Use Elementor Loop Grid + custom query filter BEFORE touching template files.
→ Only override woocommerce/ templates as an absolute last resort.
Is this a REST API endpoint?
→ Always register via register_rest_route() inside a plugin, never in functions.php.
3. Mandatory Output Format
Every single code response must follow this structure — no exceptions:
📍 PLACEMENT
Exact file path or Elementor hook location.
e.g. /wp-content/plugins/myplugin/includes/class-myplugin-hooks.php
Elementor → Site Settings → Custom Code → wp_footer
⚙️ REQUIRES
WordPress X.X+ | PHP X.X+ | Elementor Pro X.X+ | ACF X.X+ | WooCommerce X.X+
(list only what the code actually depends on)
💡 WHY THIS APPROACH
One paragraph: which branch of the §2 decision tree was taken and why.
📋 CODE
Complete, commented, deployment-ready code block — no truncation, no omissions.
🔧 INTEGRATION NOTES (include when relevant)
Any manual steps required: flush rewrite rules, set Query ID in Elementor editor,
activate plugin, clear Elementor cache, etc.
4. Quick Reference — Pattern Index
| Task |
Approach |
Sub-file |
| Plugin scaffold |
Singleton + hooks class + assets class + HPOS declaration |
scaffolding.md |
| Child theme setup |
wp_enqueue_style parent + child in functions.php |
scaffolding.md |
| Custom Post Type + Taxonomy |
register_post_type() + register_taxonomy() in plugin |
scaffolding.md |
| Secure AJAX handler |
wp_ajax_ hooks + nonce verify + wp_send_json_* |
scaffolding.md |
| PHP sanitization / escaping |
wp_unslash() + sanitize_* + esc_* patterns |
php-standards.md |
| Transient caching |
get_transient / set_transient |
php-standards.md |
| External API call + WP_Error |
wp_remote_get() + WP_Error pattern |
php-standards.md |
| Admin settings page |
Settings API + register_setting + sanitize_callback + settings_fields |
wordpress-apis.md |
| Store plugin options |
Options API + explicit boolean autoload (WP 6.6+) |
wordpress-apis.md |
| Custom field exposed to REST / Elementor |
register_post_meta + show_in_rest |
wordpress-apis.md |
| Scheduled / background task |
WP-Cron (wp_schedule_event) + Action Scheduler for heavy jobs |
wordpress-apis.md |
| Create a custom DB table |
dbDelta() + $wpdb->prefix + get_charset_collate(); strict formatting rules |
mariadb.md |
| "Specified key was too long" / emoji not saving |
utf8mb4 + index string columns at varchar(191) |
mariadb.md |
| Slow query / missing index |
EXPLAIN (type: ALL, key: NULL, Using filesort) + leftmost-prefix rule |
mariadb.md |
| SQL that must run on MariaDB and MySQL |
Plain standard SQL; JSON is stored/compared differently — don't query inside it |
mariadb.md |
| Make a plugin translatable |
i18n functions + text-domain = slug + WP 6.7 init-timing rule |
wordpress-apis.md |
| Ship a translation for a wp.org-hosted plugin |
.pot only in the zip; translate.wordpress.org → language packs (90% gate). Never bundle .mo / call load_plugin_textdomain() |
wordpress-apis.md |
| Style UI text for non-Latin locales |
Caseless scripts + text-transform/letter-spacing; don't size to English string length |
js-css-standards.md |
| Test that translations actually load |
Set the site locale before bootstrap; unload_textdomain() in WP-CLI; validate placeholders |
debugging.md |
| Deploy / tag a plugin release on wp.org SVN |
svn co → copy to trunk/ → svn cp trunk tags/X.Y.Z → svn ci (assets/ is a sibling) |
svn/svn.md |
| Any SVN task (checkout, commit, branch, merge, conflicts, repo admin) |
Subversion sub-bundle |
svn/svn.md |
| WP 6.8 password hashing |
wp_check_password() + wp_password_needs_rehash() |
php-standards.md |
| WP 6.8 app password / key hashing |
wp_fast_hash() + wp_verify_fast_hash() (BLAKE2b) |
php-standards.md |
| JS standards + enqueue defer/async |
IIFE + WP 6.3+ enqueue API |
js-css-standards.md |
| PHP → JS data passing |
wp_add_inline_script() with wp_json_encode() |
js-css-standards.md |
| CSS BEM + design tokens |
Scoped tokens, 8pt spacing, fluid type |
js-css-standards.md |
| Custom Elementor widget |
Widget_Base + all required methods |
elementor-patterns.md |
| Elementor Dynamic Tag |
Tag class + elementor/dynamic_tags/register |
elementor-patterns.md |
| Elementor Loop Grid query |
elementor/query/ filter |
elementor-patterns.md |
| ACF field in Elementor |
Dynamic Tag extending \Elementor\Core\DynamicTags\Tag |
elementor-patterns.md |
| Elementor Pro Form action |
Action_Base + field iteration |
elementor-patterns.md |
| Theme Builder custom condition |
Condition_Base + elementor/theme/register_conditions |
elementor-patterns.md |
| Elementor Pro custom form field |
Field_Base + elementor_pro/forms/fields/register |
elementor-extending.md |
| Theme Builder custom location |
register_location + elementor_theme_do_location() |
elementor-extending.md |
| Add a control to a NATIVE Elementor widget |
elementor/element/{el}/{section}/before_section_end |
elementor-extending.md |
| Filter a native widget's output |
elementor/widget/render_content filter |
elementor-extending.md |
| Add a TAB to the editor panel (like an SEO plugin) |
elementorV2.editorElementsPanel.injectTab() + elementor/editor/before_enqueue_scripts |
elementor-extending.md |
| WooCommerce HPOS compatibility |
FeaturesUtil::declare_compatibility() + wc_get_order() |
woocommerce.md |
| WooCommerce loop |
Loop Grid + elementor/query/ filter |
woocommerce.md |
| Custom REST endpoint |
register_rest_route() + schema callback in plugin |
rest-api.md |
| Off-canvas filter panel |
BEM CSS + ARIA JS + Custom Code block |
offcanvas-ui.md |
| Performance checklist |
Frontend + backend checklists |
performance.md |
| Accessibility checklist |
WCAG 2.2 AA patterns |
performance.md |
| Elementor dependency check |
Requires Plugins: elementor header + did_action fallback |
scaffolding.md |
| Widget with button/CTA |
Controls + render matching native Button widget |
widget-button.md |
| Widget with container/layout |
Controls + render matching native Container |
widget-container.md |
| Widget with image |
Group_Control_Image_Size + get_attachment_image_html() |
widget-image.md |
| Widget with heading |
TEXTAREA + header_size tag selector |
widget-heading.md |
| Widget with rich text |
WYSIWYG + wp_kses_post output |
widget-text-editor.md |
| Widget with video embed |
Source SELECT + overlay + aspect ratio |
widget-video.md |
| Widget rendering saved Elementor template |
get_builder_content_for_display() + SELECT2 |
widget-elementor-template.md |
| Widget with PHP template file |
load_template() + locate_template() strategies A/B/C |
widget-php-template.md |
| Widget with divider line |
Style + width + optional text/icon element |
widget-divider.md |
| Widget with spacer gap |
Single responsive SLIDER |
widget-spacer.md |
| Widget with single icon |
ICONS control + size + color tabs |
widget-icon.md |
| Widget with icon + text box |
Icon + title + description pattern |
widget-icon-box.md |
| Widget with image + text box |
Image + title + description pattern |
widget-image-box.md |
| Widget with image grid |
GALLERY control + Group_Control_Image_Size |
widget-image-gallery.md |
| Widget with image slider |
GALLERY + Swiper + navigation controls |
widget-image-carousel.md |
| Widget with icon bullet list |
REPEATER + icon + text + optional link |
widget-icon-list.md |
| Widget with animated counter |
Number + prefix/suffix + duration |
widget-counter.md |
| Widget with progress bar |
Percentage SLIDER + bar styling |
widget-progress.md |
| Widget with testimonial quote |
Content + image + name + job title |
widget-testimonial.md |
| Widget with tabbed panels |
REPEATER tabs + horizontal/vertical type |
widget-tabs.md |
| Widget with accordion |
REPEATER + single-open collapse pattern |
widget-accordion.md |
| Widget with toggle panels |
REPEATER + multi-open toggle pattern |
widget-toggle.md |
| Widget with social icons |
REPEATER + brand icons + links |
widget-social-icons.md |
| Widget with alert/notice box |
Type SELECT + title + description + dismiss |
widget-alert.md |
| Widget with audio player |
SoundCloud URL + autoplay options |
widget-audio.md |
| Widget outputting shortcode |
TEXTAREA + do_shortcode() |
widget-shortcode.md |
| Widget with raw HTML embed |
CODE control + unescaped output |
widget-html.md |
| Widget as named anchor |
TEXT ID + sanitize_html_class() |
widget-menu-anchor.md |
| Widget outputting sidebar |
Registered sidebar SELECT + dynamic_sidebar() |
widget-sidebar.md |
| Widget with read more tag |
No controls — WordPress $more global |
widget-read-more.md |
| Widget with Google Maps |
Address TEXT + zoom SLIDER + iframe |
widget-google-maps.md |
| Widget with star rating display |
Scale + rating number + icon style |
widget-star-rating.md |
| Widget with schema rating |
Icon count + fractional rating + gap |
widget-rating.md |
| Widget with curved/path text |
<svg> + <textPath> + unique path id |
widget-text-path.md |
| Nested Tabs / Accordion widget |
Widget_Nested_Base + print_child() + container panels |
widget-nested.md |
5. Mandatory Widget Controls — No Hardcoded Visuals (NEVER VIOLATE)
This section is MANDATORY for every custom Elementor widget. Whenever you build a widget,
every visual property must be an Elementor control — NEVER a hardcoded CSS value. Users
control appearance from the editor panel/toolbar, not by editing source code.
The Rule
NEVER hardcode any of the following in PHP render(), in static CSS, or in
content_template() output:
- Colors (text, background, border, shadow)
- Typography (font family, size, weight, line-height, letter-spacing, transform)
- Spacing (padding, margin, gap)
- Sizing (width, height, min/max values)
- Borders (style, width, color, radius)
- Shadows (box-shadow, text-shadow)
- Backgrounds (color, gradient, image)
- Alignment / positioning
- Opacity, transitions, hover effects
ALL of the above must use Elementor controls with selectors that inject CSS dynamically.
The only exceptions are structural CSS (display, position, overflow) required for the widget
layout to function at all — and even these should use controls when there is a user-facing
choice (e.g. flex-direction toggle).
⚠️ "No hardcoded visuals" does NOT mean "put a default on every control."
Elementor emits a default exactly like a user-set value, so a colour default paints on every
install and clearing the swatch only restores it — the user cannot turn it off. Elementor core
carries no default on colour controls at all (includes/widgets/heading.php: title_color
uses a 'global' reference; title_hover_color is entirely empty). Follow that split:
- Sizes, spacing, alignment, toggles, tags, labels → give a real
'default'.
- Colours → leave empty. Put the resting look in your stylesheet's
var(--token, fallback)
and let the control override it. For state colours (hover / active / selected / current)
also write a direct CSS property, never a custom property, and keep that state
colour-free in your CSS — otherwise "empty" still paints the fallback.
See field-notes.md §4 for the full trap, plus the theme-specificity and accent-color cases.
Required Controls Checklist — Apply to Every Widget
When building a widget, include ALL controls that apply to its visual elements.
Use this checklist as a mandatory gate:
| Visual property |
Required Elementor control |
Tab |
| Text content |
TEXT, TEXTAREA, or WYSIWYG + 'dynamic' => ['active' => true] |
TAB_CONTENT |
| Typography (any text element) |
add_group_control( Group_Control_Typography::get_type() ) |
TAB_STYLE |
| Text color |
COLOR control with selectors |
TAB_STYLE |
| Text alignment |
add_responsive_control() with CHOOSE (left/center/right/justify) |
TAB_STYLE or TAB_CONTENT |
| Background |
add_group_control( Group_Control_Background::get_type() ) |
TAB_STYLE |
| Border |
add_group_control( Group_Control_Border::get_type() ) |
TAB_STYLE |
| Border radius |
add_responsive_control() with DIMENSIONS + 'selectors' |
TAB_STYLE |
| Box shadow |
add_group_control( Group_Control_Box_Shadow::get_type() ) |
TAB_STYLE |
| Text shadow |
add_group_control( Group_Control_Text_Shadow::get_type() ) |
TAB_STYLE |
| Padding |
add_responsive_control() with DIMENSIONS |
TAB_STYLE |
| Margin |
add_responsive_control() with DIMENSIONS |
TAB_STYLE |
| Width / Height |
add_responsive_control() with SLIDER |
TAB_STYLE |
| Spacing / Gap |
add_responsive_control() with SLIDER |
TAB_STYLE |
| Image |
MEDIA + add_group_control( Group_Control_Image_Size::get_type() ) |
TAB_CONTENT |
| CSS Filters (if image/element) |
add_group_control( Group_Control_Css_Filter::get_type() ) |
TAB_STYLE |
| Hover state |
Duplicate color/background/shadow controls inside 'section_style_hover' with selectors targeting :hover |
TAB_STYLE |
| Transition duration |
SLIDER (seconds) with selectors => ['transition-duration'] |
TAB_STYLE |
| Link |
URL control with 'dynamic' => ['active' => true] |
TAB_CONTENT |
| Icon |
ICONS control with fa4compatibility |
TAB_CONTENT |
| HTML tag |
SELECT (h1–h6, div, span, p) |
TAB_CONTENT |
Example — Correct vs Incorrect
// ❌ WRONG — hardcoded color and font-size
protected function render(): void {
$settings = $this->get_settings_for_display();
echo '<h2 style="color: #e94560; font-size: 24px;">'
. esc_html( $settings['title'] ) . '</h2>';
}
// ✅ CORRECT — all visuals controlled from the panel via selectors
protected function register_controls(): void {
// ... Content section with title TEXT control ...
$this->start_controls_section( 'section_title_style', [
'label' => esc_html__( 'Title Style', 'myplugin' ),
'tab' => \Elementor\Controls_Manager::TAB_STYLE,
] );
$this->add_control( 'title_color', [
'label' => esc_html__( 'Color', 'myplugin' ),
'type' => \Elementor\Controls_Manager::COLOR,
'selectors' => [
'{{WRAPPER}} .myplugin-widget__title' => 'color: {{VALUE}};',
],
] );
$this->add_group_control(
\Elementor\Group_Control_Typography::get_type(),
[
'name' => 'title_typography',
'selector' => '{{WRAPPER}} .myplugin-widget__title',
]
);
$this->add_responsive_control( 'title_align', [
'label' => esc_html__( 'Alignment', 'myplugin' ),
'type' => \Elementor\Controls_Manager::CHOOSE,
'options' => [
'left' => [ 'title' => esc_html__( 'Left', 'myplugin' ), 'icon' => 'eicon-text-align-left' ],
'center' => [ 'title' => esc_html__( 'Center', 'myplugin' ), 'icon' => 'eicon-text-align-center' ],
'right' => [ 'title' => esc_html__( 'Right', 'myplugin' ), 'icon' => 'eicon-text-align-right' ],
],
'selectors' => [
'{{WRAPPER}} .myplugin-widget__title' => 'text-align: {{VALUE}};',
],
] );
$this->add_responsive_control( 'title_spacing', [
'label' => esc_html__( 'Bottom Spacing', 'myplugin' ),
'type' => \Elementor\Controls_Manager::SLIDER,
'size_units' => [ 'px', 'em', 'rem' ],
'range' => [ 'px' => [ 'min' => 0, 'max' => 100 ] ],
'selectors' => [
'{{WRAPPER}} .myplugin-widget__title' => 'margin-bottom: {{SIZE}}{{UNIT}};',
],
] );
$this->end_controls_section();
}
protected function render(): void {
$settings = $this->get_settings_for_display();
// ✅ No inline styles — all visuals come from Elementor's selectors
$this->add_render_attribute( 'title', 'class', 'myplugin-widget__title' );
$this->add_inline_editing_attributes( 'title' );
echo '<h2 ' . $this->get_render_attribute_string( 'title' ) . '>'
. esc_html( $settings['title'] ) . '</h2>';
}
add_render_attribute() and add_inline_editing_attributes() — Always Use
The official Elementor API for building HTML attributes is $this->add_render_attribute().
Always use it instead of manually concatenating class/id/aria attributes in render().
Pair with $this->add_inline_editing_attributes() for any text field that supports live
editing in the Elementor editor panel.
Outputting the built attributes — pick by context: use
get_render_attribute_string( 'key' ) when you are concatenating into a string (as in the
echo '<h2 ' . ... . '>' example above), and print_render_attribute_string( 'key' ) when
you are echoing directly inside a ?> … <?php HTML block (e.g.
<h2 <?php $this->print_render_attribute_string( 'title' ); ?>>). Both are correct Elementor
APIs — print_* simply echoes what get_* returns. The widget sub-files use the print_*
form inside their HTML templates.
Source: developers.elementor.com/docs/widgets/rendering-html-attribute/
Source: developers.elementor.com/docs/widgets/rendering-inline-editing/
1---2name: wordpress-elementor-dev3description: Expert WordPress and Elementor Pro development skill. Use this whenever the user asks about WordPress theme development, Elementor Pro customization, plugin architecture, custom post types, hooks/filters, Loop Grids, Dynamic Tags, Theme Builder templates, off-canvas UIs, custom CSS/JS injection, performance optimization, or any PHP/JS/CSS code targeting a WordPress or Elementor environment. Trigger even for general questions like "how do I add a custom field in Elementor", "what hook should I use for X in WordPress", "how do I override a WooCommerce template", or "how do I register a custom widget" — do not wait for the user to explicitly say "use the skill." Also trigger for ACF integration, REST API endpoints, child theme setup, WooCommerce customization, WPML/multilingual setups, and any debugging of WordPress/Elementor issues.4---56# WordPress & Elementor Pro — Skill Router78A complete procedural guide for producing production-grade WordPress and Elementor Pro9code. This file is the **router** — read it first, then load the relevant sub-file(s)10for the task at hand.1112---1314## Sub-file Map — Read the Right File for Each Task1516| Task type | Read this file |17|---|---|18| Plugin scaffold, child theme, code placement, CPT, AJAX handler | **scaffolding.md** |19| PHP standards, sanitization, escaping, nonces, WP_Error, transients, password hashing | **php-standards.md** |20| JavaScript standards, enqueue API, defer/async, wp_add_inline_script | **js-css-standards.md** |21| CSS standards, BEM, design tokens, Elementor CSS selectors | **js-css-standards.md** |22| Elementor custom widget, Dynamic Tags, Loop Grid, Form actions, Theme Builder conditions | **elementor-patterns.md** |23| Elementor extension points — custom **form fields**, **theme locations**, injecting controls into native widgets, **adding a TAB to the editor's Elements panel**, Finder/context-menu, hooks reference, deprecations | **elementor-extending.md** |24| WooCommerce HPOS, order API, template overrides, Loop Grid for products | **woocommerce.md** |25| REST API endpoints, schema, permission callbacks | **rest-api.md** |26| Off-canvas UI, off-canvas accessibility, focus trap | **offcanvas-ui.md** |27| Performance checklists (frontend + backend), speculative loading, IE conditional comments | **performance.md** |28| Accessibility checklist, WCAG 2.2 AA, ARIA patterns | **performance.md** |29| **Hard-won production gotchas** — widget lifecycle fatals, `content_template()` escaping, CSS-in-Elementor footguns, transactional email, wp.org review/packaging, embedding apps, AJAX/loop re-rendering, dynamic CSS in loops, Swiper on optimised sites | **field-notes.md** |30| **wordpress.org submission** — the 18 Directory Guidelines, Plugin Check 2.1.0 categories/usage, review process, required headers/readme | **wp-org-guidelines.md** |31| **Subversion (SVN)** — deploying/tagging a release on the wordpress.org plugin/theme SVN (trunk/tags/assets), the daily work cycle, branching/merging, properties, repo admin | **svn/svn.md** (self-contained sub-bundle; `svn/references/` goes deeper) |32| **Debugging & static analysis** — PHPCS+WPCS, PHPStan, Plugin Check, `WP_DEBUG`/Query Monitor, Elementor Safe Mode/cache, symptom→cause table | **debugging.md** |33| **Common WordPress APIs** — admin settings page (Settings + Options API), `register_meta`, roles/capabilities, WP-Cron, internationalization (i18n) | **wordpress-apis.md** |34| **Database layer (MariaDB/MySQL)** — custom tables + `dbDelta()`, charsets & the `varchar(191)` index rule, indexes, `EXPLAIN`, MariaDB↔MySQL portability, `sql_mode`, full-text | **mariadb.md** |3536### Widget Boilerplates — Load when building a widget of that type3738| Widget type | Read this file |39|---|---|40| Button / CTA with icon and link | **widget-button.md** |41| Container / layout wrapper / section / card shell | **widget-container.md** |42| Image with caption, link, lightbox | **widget-image.md** |43| Heading / title / HTML tag selector | **widget-heading.md** |44| Rich text / WYSIWYG body content | **widget-text-editor.md** |45| Video embed (YouTube, Vimeo, self-hosted) | **widget-video.md** |46| Widget that renders a saved Elementor template by ID | **widget-elementor-template.md** (SELECT2, `get_builder_content_for_display()`, Dynamic Tags context, CSS timing) |47| Widget whose markup lives in a separate PHP template file | **widget-php-template.md** (Strategies A/B/C, `load_template()`, `ob_start`, path-traversal safety) |48| Divider / horizontal rule with optional text or icon | **widget-divider.md** |49| Spacer / vertical gap | **widget-spacer.md** |50| Single standalone icon with optional link | **widget-icon.md** |51| Icon + title + description box | **widget-icon-box.md** |52| Image + title + description box | **widget-image-box.md** |53| Image grid / gallery | **widget-image-gallery.md** |54| Image slider / carousel | **widget-image-carousel.md** |55| Bullet list with icons per item | **widget-icon-list.md** |56| Animated number counter | **widget-counter.md** |57| Percentage progress bar | **widget-progress.md** |58| Customer quote / testimonial | **widget-testimonial.md** |59| Tabbed content panels | **widget-tabs.md** |60| Accordion — one panel open at a time | **widget-accordion.md** |61| Toggle — multiple panels open simultaneously | **widget-toggle.md** |62| Social media icon links row | **widget-social-icons.md** |63| Colored alert / notice box | **widget-alert.md** |64| Audio player (SoundCloud / self-hosted) | **widget-audio.md** |65| WordPress shortcode output | **widget-shortcode.md** |66| Raw custom HTML / JS / CSS embed | **widget-html.md** |67| Named anchor for in-page navigation | **widget-menu-anchor.md** |68| WordPress registered sidebar output | **widget-sidebar.md** |69| WordPress <!--more--> read more tag | **widget-read-more.md** |70| Google Maps embed | **widget-google-maps.md** |71| Decorative star rating display | **widget-star-rating.md** |72| Schema-ready structured rating | **widget-rating.md** |73| Text following a curved / custom SVG path | **widget-text-path.md** |74| Nested Tabs / Nested Accordion (each panel is a Container holding any widgets) | **widget-nested.md** |7576**Always read the relevant sub-file before writing code.** For tasks that span multiple77areas (e.g. a WooCommerce widget with custom REST endpoint), read all relevant sub-files.78For a widget task, read BOTH the widget boilerplate file AND **elementor-patterns.md**.79For **any custom widget, plugin, or wp.org-bound work, also skim `field-notes.md`** — it80catches the lint-passing, review-failing, site-down mistakes the topic files don't dwell on.8182---8384## 0. Golden Rules (Never Violate)8586These override everything in all sub-files:87881. **Native APIs first** — WordPress core hook before plugin; Elementor API before template override.892. **Sanitize in, escape out** — Every input sanitized. Every output escaped. No exceptions.903. **Prefix everything** — All functions, classes, constants, hooks, and CSS classes use a project-specific prefix.914. **State your placement** — Every code response must declare exactly where the code lives.925. **No over-clarifying** — Only ask a clarification question if the missing info would materially change the code output. Otherwise, state your assumption and proceed.936. **NEVER hardcode visual settings in widgets** — Every visual property (colors, fonts, sizes,94 spacing, backgrounds, borders, shadows, alignment) MUST be exposed as a standard Elementor95 control in the editor panel. Users control appearance via the toolbar — not by editing code.96 See §5 "Mandatory Widget Controls" below for the required controls checklist.977. **Name for the directory from day one** — Naming is decided first and is effectively98 irreversible, so get it right before writing code. The plugin **slug/name must NOT start with99 a trademark you don't own** — `"CRM for WooCommerce"`, never `"WooCommerce CRM"` (Directory100 Guideline 17). The **text domain must exactly equal the plugin slug**. The wp.org slug is101 **permanent**, and a widget's `get_name()` is **sticky** (stored in every page's102 `_elementor_data` — renaming it breaks placed widgets). Choose the public name, slug, text103 domain, and code prefix (Rule #3) **together, once**. This is distinct from Rule #3: that104 governs internal code symbols; this governs the public identity. See **wp-org-guidelines.md**105 (Guidelines 12/16/17) and **field-notes.md** §1 (sticky `get_name()`).106107---108109## 1. Default Assumptions110111Quickly assess — **only ask if the answer would change the code**:112113| Info needed | Ask only if... |114|---|---|115| Scope (plugin vs snippet vs child theme) | Context doesn't make it obvious |116| Elementor tier (Free / Pro / custom widget) | Pro-only APIs are involved |117| PHP version | Code uses PHP 8.3+ features like typed class constants, or 8.4+ features like property hooks |118| WooCommerce / ACF / WPML present | Integration with those systems is required |119120**Default stack when not stated** (full release-by-release history lives in **CHANGELOG.md** — keep volatile version-tracking out of this router):121122| Component | Version | Notes |123|---|---|---|124| **WordPress** | **7.1** | **"Mary Lou", released Aug 19, 2026** (7.0 "Armstrong" was May 20; the 7.0 branch ended at 7.0.4, Aug 12). Minimum PHP **7.4** (7.2/7.3 dropped — sites still on them stay pinned to 6.9.x). No multisite assumed. **7.1 makes the post editor iframe unconditional — see below.** |125| **PHP** | **8.3** recommended | 7.4 = minimum. 8.4 / 8.5 = "beta support" (possible deprecation notices). 8.2 fully compatible but no longer the recommended default. |126| **Elementor (free + Pro)** | **4.2+** | Separate plugins with **independent version numbers** — currently **free 4.2.3 / Pro 4.2.2** (both Aug 19, 2026). 4.0.0 (Mar 30, 2026) made the Atomic Editor stable + default for new installs; 4.2.0 (Jul 20) added Atomic Grid (free) and Atomic Loop (Pro). ⚠️ Elementor's header still reads **"Tested up to: 7.0.4"** — it has **not** declared WP 7.1 support. **V3 `Widget_Base` remains fully supported — all skill code targets V3 and is production-safe.** |127| **WooCommerce** | **11.0+** | **11.0 (Aug 4, 2026) REMOVED the product editor beta** and bumped to **Action Scheduler 4.0.0** (breaking dedup change). HPOS default-on since 8.2; 10.7 disabled HPOS "sync on read"; 10.9 defers Store API draft-order creation — see woocommerce.md. |128129**Note:** Elementor core and Elementor Pro have independent version numbers — always check **both** when diagnosing compatibility issues.130131### WordPress 7.1 — what changed for plugin / Elementor devs132133WP 7.1 **"Mary Lou"** shipped **August 19, 2026** (WordCamp US). Unlike 7.0, this release134contains **four changes that can break a shipped plugin** — they are listed first. No PHP or135WordPress minimum changed.136137**🔴 Breaking — audit your plugin for these:**1381391. **The post editor is now ALWAYS in an iframe** — including on sites registering legacy meta140 boxes, which was the last remaining escape hatch. Editor JavaScript that reaches for the141 global `document` / `window` now targets the **wrong document**. Use the canvas element's142 **`ownerDocument`** and **`defaultView`** instead:143 ```js144 // ❌ breaks in 7.1 — this is the OUTER document, not the editor canvas145 document.querySelector( '.my-block' ).classList.add( 'ready' );146147 // ✅ resolve the document from a node you already own inside the canvas148 const doc = myCanvasNode.ownerDocument;149 const view = doc.defaultView; // the iframe's `window`150 doc.querySelector( '.my-block' )?.classList.add( 'ready' );151 view.requestAnimationFrame( … );152 ```153 Injected `<style>`/`<link>` must go into the **canvas** document too, or it styles nothing.154 **Elementor's own editor is unaffected** (it is not the block editor) — this hits block155 registrations, meta boxes, and any block-editor integration your plugin ships.1562. **`__next40pxDefaultSize` is now a no-op** on `@wordpress/components` — **remove the prop**157 entirely; there is no replacement. Form controls render at 40px unconditionally.1583. **List-table markup changed:** the row header moved from the checkbox column to the **title**159 column. Any CSS/JS selector keyed on `th.check-column` (or assuming the row header's position)160 breaks — audit custom admin list tables and column callbacks.1614. **jQuery UI updated to 1.14.2.** Test anything depending on jQuery UI behaviour or styling.162163**🟢 New and useful:**164165- **SVG Icon API** (public in 7.1) — register icons and whole collections, render server-side,166 read over REST: `wp_register_icon_collection()`, `wp_register_icon()`, `wp_get_icon()`.167 ⚠️ SVGs pass through a **conservative allowlist — only `<svg>`, `<path>` and `<polygon>` survive**.168 A `<g>`, `<circle>`, `<rect>` or `<use>` in your icon is stripped silently, so convert shapes to169 paths before registering. (This is a *core* API — Elementor widgets still ship their own SVGs;170 see `elementor-extending.md` §8 and the "don't `wp_kses()` inline SVG" rule in `field-notes.md` §6.)171- **Abilities API matured:** `wp_get_abilities()` filtering, **execution-lifecycle hooks**, custom172 validation, a unified `public` exposure flag, and client-compatible JSON-Schema preparation.173 Registration timing rules from 7.0 are unchanged (see the 7.0 section below).174- **Global Styles / `theme.json`:** responsive style variations with **configurable breakpoints**,175 pseudo-state styling (`:hover` / `:focus` / `:active`), and `text-shadow` support.176- **Media:** client-side image processing in the browser via **WebAssembly** (compression/resizing177 no longer hit the server), infinite scroll in the Media Library, and registration of multiple178 image sizes at once. Multisite now enforces upload limits on **media sideloading**.179- **Persistent admin bar** across editor screens — review any custom toolbar nodes for behaviour180 in the editor context.181- **Design System:** semantic design tokens for theming admin UI (relevant if you build admin or182 editor panels — compare the Elementor panel tokens in `elementor-extending.md` §5).183- **DataViews / DataForm** APIs matured for data-driven admin interfaces.184- **React stays on 18.3** — React 19 was deferred again. Don't bundle `react/jsx-runtime`185 yourself, and avoid string refs / `defaultProps`, which fail under the experimental React 19 flag.186- Conditional block-CSS loading can affect **remote content pulls** — verify if you render blocks187 outside a normal page request.188189_Sources: wordpress.org/news/2026/08/mary-lou/ · make.wordpress.org/core/2026/08/05/wordpress-7-1-field-guide/ ·190developer.wordpress.org/news/2026/08/whats-new-for-developers-august-2026/_191192### WordPress 7.0 — what changed for plugin / Elementor devs193194WP 7.0 "Armstrong" shipped **May 20, 2026** (delayed from the original April 9 target while195the RTC storage layer was redesigned — see below). Everything below is **opt-in and196non-breaking**; most plugin/Elementor work is unaffected. It remains relevant because 7.1197changed none of it.198199- **Minimum PHP is now 7.4** (7.2/7.3 dropped). The skill's recommended baseline stays200 **PHP 8.3**. Bump your plugin's `Requires PHP` header to 7.4 only once you target WP 7.0+201 exclusively. No new DB minimum is enforced; `wordpress.org/about/requirements/` recommends202 **MariaDB 10.11+ or MySQL 8.0+** (the MariaDB floor was raised from 10.6; re-verified Aug 2026).203- **Real-Time Collaboration (RTC):** simultaneous multi-author block editing (CRDT-based, via204 an HTTP-polling sync provider — not WebRTC). Data is stored in a **dedicated core database205 table**; an earlier `wp_post_meta` / `wp_sync_storage` design was rejected, and building the206 table is what pushed the release from April to May. **Plugin impact:** scope every207 `WP_Query` / `get_posts()` with an explicit `post_type` so internal core post types never208 leak into your results — do **not** hardcode any internal RTC type name. The209 `WP_ALLOW_COLLABORATION` constant lets hosts swap the sync transport.210- **WP AI Client:** provider-agnostic PHP + JS AI API — `wp_ai_client_prompt( $prompt )->generate_text()`.211 Guard with `function_exists( 'wp_ai_client_prompt' )`.212- **Abilities API:** `wp_register_ability()` (PHP, since WP 6.9) plus a JS counterpart in 7.0.213 Use `'meta' => ['show_in_rest' => true]` to expose via REST. Register ONLY inside the214 **`wp_abilities_api_init`** hook (core renamed the feature plugin's `abilities_api_init`;215 categories go on `wp_abilities_api_categories_init`) or you get `_doing_it_wrong`; a216 `category` is required and must already be registered. Calling it also sets your real217 `Requires at least` floor to **6.9** — Plugin Check errors on the header and218 `function_exists()` guards don't count (see `debugging.md` §1).219- **Connectors UI** (Settings → Connectors) for managing AI provider credentials, and a220 **Command Palette** in wp-admin.221- **Iframed editor** was still conditional in 7.0 — **7.1 makes it unconditional.** See the 7.1222 section above; `"apiVersion": 3` in `block.json` is now table stakes, not preparation.223224_Sources: make.wordpress.org/core/2026/01/09/dropping-support-for-php-7-2-and-7-3/ ·225make.wordpress.org/core/2026/04/22/wordpress-7-0-release-party-updated-schedule/ ·226wordpress.org/about/requirements/_227228> 📌 **PHP support labels (unchanged in WP 7.1):** PHP 7.4–8.3 fully compatible; **8.3 recommended**;229> 8.4 (WP 6.7+) and 8.5 (WP 6.9+) carry a "beta support" label (possible deprecation notices).230> Source: make.wordpress.org/core/handbook/references/php-compatibility-and-wordpress-versions/231232> ✅ **Elementor 4.x status (current: free 4.2.3 / Pro 4.2.2, both Aug 19, 2026):** Elementor233> 4.0.0 (Mar 30, 2026, free + Pro) made the **Atomic Editor stable and the default for new234> installs** and added Atomic Forms, Pro Interactions, and Component creation. Updating to 4.x235> leaves **existing sites untouched** — V3 widgets and V4 Atomic Elements coexist on the same236> page; Atomic features are toggled at WP Admin → Elementor → Editor → Settings.237>238> **There is still NO third-party API for building Atomic Elements, and Elementor has said so239> explicitly** — they will not release one soon and *advise against* integrating with Atomic240> internals until they announce it (elementor/elementor GitHub Discussion #32950). Atomic241> Elements are documented only as a **data structure**242> (developers.elementor.com/docs/data-structure/atomic-elements). So **continue using V3243> `Widget_Base`** for all third-party widgets — it is the correct, production-safe API and all244> skill code targets it. *(Re-verified Aug 23, 2026 — unchanged since the July check.)*245>246> ⚠️ **Elementor has not declared WP 7.1 compatibility yet.** As of free 4.2.3 / Pro 4.2.2 the247> `Tested up to` header reads **7.0.4**, three days after WP 7.1 shipped. That is normal lag,248> not a known incompatibility — but when a client reports editor breakage on a fresh 7.1 site,249> check Elementor's current `Tested up to` before debugging your own code.250>251> **V4 Atomic Elements that now ship by default (awareness only — not third-party-buildable yet):**252> Div Block & Flexbox Container (layout); **Atomic Grid** — advanced row/column layouts (free253> 4.2.0, Jul 20, 2026); Atomic Heading, Paragraph, Image, Button, Video, SVG;254> **Atomic Tabs**; **Atomic Forms** (Pro) with composable fields — Label, Input, Textarea,255> Checkbox, Submit, plus Radio, Select, Date Picker, Time Picker, and File Upload (added in256> Pro 4.1.0, May 26, 2026); and **Atomic Loop** — dynamic content layouts, the V4 counterpart257> of the Pro Loop Grid (Pro 4.2.0, Jul 20, 2026). These are end-user elements; building258> **custom** atomic elements still awaits the finalized V4 extension docs — keep targeting V3259> `Widget_Base` until then.260> Source: elementor.com/products/website-builder/v4-faq/ ·261> developers.elementor.com/elementor-editor-4-0-developers-update/ ·262> elementor.com/pro/changelog/ · github.com/elementor/elementor/releases263264> 🗓️ **Release-by-release history (betas, RCs, point releases) lives in `CHANGELOG.md`.**265> Keep this router focused on durable guidance; update version facts in the table above266> and in `CHANGELOG.md`, not scattered across the sub-files.267---268269## 2. Architecture Decision Tree270271Run through this mentally before writing a single line:272273```274Does a WordPress core hook (add_action/add_filter) solve it?275 YES → Use the hook. No plugin needed. Place in child theme functions.php276 or Elementor Custom Code.277 NO → Does Elementor's PHP/JS API solve it?278 YES → Extend via Elementor hooks, Dynamic Tags, or Widget_Base.279 Register via elementor/widgets/register or elementor/dynamic_tags/register.280 NO → Is this logic reusable across themes or sites?281 YES → Scaffold a dedicated plugin (see scaffolding.md).282 NO → Child theme functions.php or Elementor Custom Code block.283284Is a WooCommerce override needed?285 → Use Elementor Loop Grid + custom query filter BEFORE touching template files.286 → Only override woocommerce/ templates as an absolute last resort.287288Is this a REST API endpoint?289 → Always register via register_rest_route() inside a plugin, never in functions.php.290```291292---293294## 3. Mandatory Output Format295296**Every single code response must follow this structure — no exceptions:**297298```299📍 PLACEMENT300Exact file path or Elementor hook location.301e.g. /wp-content/plugins/myplugin/includes/class-myplugin-hooks.php302 Elementor → Site Settings → Custom Code → wp_footer303304⚙️ REQUIRES305WordPress X.X+ | PHP X.X+ | Elementor Pro X.X+ | ACF X.X+ | WooCommerce X.X+306(list only what the code actually depends on)307308💡 WHY THIS APPROACH309One paragraph: which branch of the §2 decision tree was taken and why.310311📋 CODE312Complete, commented, deployment-ready code block — no truncation, no omissions.313314🔧 INTEGRATION NOTES (include when relevant)315Any manual steps required: flush rewrite rules, set Query ID in Elementor editor,316activate plugin, clear Elementor cache, etc.317```318319---320321## 4. Quick Reference — Pattern Index322323| Task | Approach | Sub-file |324|---|---|---|325| Plugin scaffold | Singleton + hooks class + assets class + HPOS declaration | scaffolding.md |326| Child theme setup | `wp_enqueue_style` parent + child in functions.php | scaffolding.md |327| Custom Post Type + Taxonomy | `register_post_type()` + `register_taxonomy()` in plugin | scaffolding.md |328| Secure AJAX handler | `wp_ajax_` hooks + nonce verify + `wp_send_json_*` | scaffolding.md |329| PHP sanitization / escaping | `wp_unslash()` + `sanitize_*` + `esc_*` patterns | php-standards.md |330| Transient caching | `get_transient` / `set_transient` | php-standards.md |331| External API call + WP_Error | `wp_remote_get()` + `WP_Error` pattern | php-standards.md |332| Admin settings page | Settings API + `register_setting` + `sanitize_callback` + `settings_fields` | wordpress-apis.md |333| Store plugin options | Options API + explicit boolean `autoload` (WP 6.6+) | wordpress-apis.md |334| Custom field exposed to REST / Elementor | `register_post_meta` + `show_in_rest` | wordpress-apis.md |335| Scheduled / background task | WP-Cron (`wp_schedule_event`) + Action Scheduler for heavy jobs | wordpress-apis.md |336| Create a custom DB table | `dbDelta()` + `$wpdb->prefix` + `get_charset_collate()`; strict formatting rules | mariadb.md |337| "Specified key was too long" / emoji not saving | `utf8mb4` + index string columns at **`varchar(191)`** | mariadb.md |338| Slow query / missing index | `EXPLAIN` (`type: ALL`, `key: NULL`, `Using filesort`) + leftmost-prefix rule | mariadb.md |339| SQL that must run on MariaDB **and** MySQL | Plain standard SQL; JSON is stored/compared differently — don't query inside it | mariadb.md |340| Make a plugin translatable | i18n functions + text-domain = slug + WP 6.7 `init`-timing rule | wordpress-apis.md |341| Ship a translation for a wp.org-hosted plugin | `.pot` only in the zip; translate.wordpress.org → language packs (90% gate). **Never** bundle `.mo` / call `load_plugin_textdomain()` | wordpress-apis.md |342| Style UI text for non-Latin locales | Caseless scripts + `text-transform`/`letter-spacing`; don't size to English string length | js-css-standards.md |343| Test that translations actually load | Set the site locale before bootstrap; `unload_textdomain()` in WP-CLI; validate placeholders | debugging.md |344| Deploy / tag a plugin release on wp.org SVN | `svn co` → copy to `trunk/` → `svn cp trunk tags/X.Y.Z` → `svn ci` (assets/ is a sibling) | svn/svn.md |345| Any SVN task (checkout, commit, branch, merge, conflicts, repo admin) | Subversion sub-bundle | svn/svn.md |346| WP 6.8 password hashing | `wp_check_password()` + `wp_password_needs_rehash()` | php-standards.md |347| WP 6.8 app password / key hashing | `wp_fast_hash()` + `wp_verify_fast_hash()` (BLAKE2b) | php-standards.md |348| JS standards + enqueue defer/async | IIFE + WP 6.3+ enqueue API | js-css-standards.md |349| PHP → JS data passing | `wp_add_inline_script()` with `wp_json_encode()` | js-css-standards.md |350| CSS BEM + design tokens | Scoped tokens, 8pt spacing, fluid type | js-css-standards.md |351| Custom Elementor widget | `Widget_Base` + all required methods | elementor-patterns.md |352| Elementor Dynamic Tag | `Tag` class + `elementor/dynamic_tags/register` | elementor-patterns.md |353| Elementor Loop Grid query | `elementor/query/` filter | elementor-patterns.md |354| ACF field in Elementor | Dynamic Tag extending `\Elementor\Core\DynamicTags\Tag` | elementor-patterns.md |355| Elementor Pro Form action | `Action_Base` + field iteration | elementor-patterns.md |356| Theme Builder custom condition | `Condition_Base` + `elementor/theme/register_conditions` | elementor-patterns.md |357| Elementor Pro custom form field | `Field_Base` + `elementor_pro/forms/fields/register` | elementor-extending.md |358| Theme Builder custom location | `register_location` + `elementor_theme_do_location()` | elementor-extending.md |359| Add a control to a NATIVE Elementor widget | `elementor/element/{el}/{section}/before_section_end` | elementor-extending.md |360| Filter a native widget's output | `elementor/widget/render_content` filter | elementor-extending.md |361| Add a TAB to the editor panel (like an SEO plugin) | `elementorV2.editorElementsPanel.injectTab()` + `elementor/editor/before_enqueue_scripts` | elementor-extending.md |362| WooCommerce HPOS compatibility | `FeaturesUtil::declare_compatibility()` + `wc_get_order()` | woocommerce.md |363| WooCommerce loop | Loop Grid + `elementor/query/` filter | woocommerce.md |364| Custom REST endpoint | `register_rest_route()` + schema callback in plugin | rest-api.md |365| Off-canvas filter panel | BEM CSS + ARIA JS + Custom Code block | offcanvas-ui.md |366| Performance checklist | Frontend + backend checklists | performance.md |367| Accessibility checklist | WCAG 2.2 AA patterns | performance.md |368| Elementor dependency check | `Requires Plugins: elementor` header + `did_action` fallback | scaffolding.md |369| Widget with button/CTA | Controls + render matching native Button widget | widget-button.md |370| Widget with container/layout | Controls + render matching native Container | widget-container.md |371| Widget with image | `Group_Control_Image_Size` + `get_attachment_image_html()` | widget-image.md |372| Widget with heading | TEXTAREA + header_size tag selector | widget-heading.md |373| Widget with rich text | WYSIWYG + wp_kses_post output | widget-text-editor.md |374| Widget with video embed | Source SELECT + overlay + aspect ratio | widget-video.md |375| Widget rendering saved Elementor template | get_builder_content_for_display() + SELECT2 | widget-elementor-template.md |376| Widget with PHP template file | load_template() + locate_template() strategies A/B/C | widget-php-template.md |377| Widget with divider line | Style + width + optional text/icon element | widget-divider.md |378| Widget with spacer gap | Single responsive SLIDER | widget-spacer.md |379| Widget with single icon | ICONS control + size + color tabs | widget-icon.md |380| Widget with icon + text box | Icon + title + description pattern | widget-icon-box.md |381| Widget with image + text box | Image + title + description pattern | widget-image-box.md |382| Widget with image grid | GALLERY control + Group_Control_Image_Size | widget-image-gallery.md |383| Widget with image slider | GALLERY + Swiper + navigation controls | widget-image-carousel.md |384| Widget with icon bullet list | REPEATER + icon + text + optional link | widget-icon-list.md |385| Widget with animated counter | Number + prefix/suffix + duration | widget-counter.md |386| Widget with progress bar | Percentage SLIDER + bar styling | widget-progress.md |387| Widget with testimonial quote | Content + image + name + job title | widget-testimonial.md |388| Widget with tabbed panels | REPEATER tabs + horizontal/vertical type | widget-tabs.md |389| Widget with accordion | REPEATER + single-open collapse pattern | widget-accordion.md |390| Widget with toggle panels | REPEATER + multi-open toggle pattern | widget-toggle.md |391| Widget with social icons | REPEATER + brand icons + links | widget-social-icons.md |392| Widget with alert/notice box | Type SELECT + title + description + dismiss | widget-alert.md |393| Widget with audio player | SoundCloud URL + autoplay options | widget-audio.md |394| Widget outputting shortcode | TEXTAREA + do_shortcode() | widget-shortcode.md |395| Widget with raw HTML embed | CODE control + unescaped output | widget-html.md |396| Widget as named anchor | TEXT ID + sanitize_html_class() | widget-menu-anchor.md |397| Widget outputting sidebar | Registered sidebar SELECT + dynamic_sidebar() | widget-sidebar.md |398| Widget with read more tag | No controls — WordPress $more global | widget-read-more.md |399| Widget with Google Maps | Address TEXT + zoom SLIDER + iframe | widget-google-maps.md |400| Widget with star rating display | Scale + rating number + icon style | widget-star-rating.md |401| Widget with schema rating | Icon count + fractional rating + gap | widget-rating.md |402| Widget with curved/path text | `<svg>` + `<textPath>` + unique path id | widget-text-path.md |403| Nested Tabs / Accordion widget | `Widget_Nested_Base` + `print_child()` + container panels | widget-nested.md |404405---406407## 5. Mandatory Widget Controls — No Hardcoded Visuals (NEVER VIOLATE)408409> **This section is MANDATORY for every custom Elementor widget.** Whenever you build a widget,410> every visual property must be an Elementor control — NEVER a hardcoded CSS value. Users411> control appearance from the editor panel/toolbar, not by editing source code.412413### The Rule414415**NEVER hardcode** any of the following in PHP `render()`, in static CSS, or in416`content_template()` output:417418- Colors (text, background, border, shadow)419- Typography (font family, size, weight, line-height, letter-spacing, transform)420- Spacing (padding, margin, gap)421- Sizing (width, height, min/max values)422- Borders (style, width, color, radius)423- Shadows (box-shadow, text-shadow)424- Backgrounds (color, gradient, image)425- Alignment / positioning426- Opacity, transitions, hover effects427428**ALL** of the above must use Elementor controls with `selectors` that inject CSS dynamically.429The only exceptions are structural CSS (display, position, overflow) required for the widget430layout to function at all — and even these should use controls when there is a user-facing431choice (e.g. flex-direction toggle).432433> ⚠️ **"No hardcoded visuals" does NOT mean "put a `default` on every control."**434> Elementor emits a `default` exactly like a user-set value, so a colour default paints on every435> install and clearing the swatch only restores it — the user cannot turn it off. **Elementor core436> carries no `default` on colour controls at all** (`includes/widgets/heading.php`: `title_color`437> uses a `'global'` reference; `title_hover_color` is entirely empty). Follow that split:438> - **Sizes, spacing, alignment, toggles, tags, labels** → give a real `'default'`.439> - **Colours** → leave empty. Put the resting look in your stylesheet's `var(--token, fallback)`440> and let the control override it. For **state** colours (hover / active / selected / current)441> also write a **direct CSS property**, never a custom property, and keep that state442> **colour-free in your CSS** — otherwise "empty" still paints the fallback.443>444> See `field-notes.md` §4 for the full trap, plus the theme-specificity and `accent-color` cases.445446### Required Controls Checklist — Apply to Every Widget447448When building a widget, include ALL controls that apply to its visual elements.449Use this checklist as a mandatory gate:450451| Visual property | Required Elementor control | Tab |452|---|---|---|453| **Text content** | `TEXT`, `TEXTAREA`, or `WYSIWYG` + `'dynamic' => ['active' => true]` | TAB_CONTENT |454| **Typography** (any text element) | `add_group_control( Group_Control_Typography::get_type() )` | TAB_STYLE |455| **Text color** | `COLOR` control with `selectors` | TAB_STYLE |456| **Text alignment** | `add_responsive_control()` with `CHOOSE` (left/center/right/justify) | TAB_STYLE or TAB_CONTENT |457| **Background** | `add_group_control( Group_Control_Background::get_type() )` | TAB_STYLE |458| **Border** | `add_group_control( Group_Control_Border::get_type() )` | TAB_STYLE |459| **Border radius** | `add_responsive_control()` with `DIMENSIONS` + `'selectors'` | TAB_STYLE |460| **Box shadow** | `add_group_control( Group_Control_Box_Shadow::get_type() )` | TAB_STYLE |461| **Text shadow** | `add_group_control( Group_Control_Text_Shadow::get_type() )` | TAB_STYLE |462| **Padding** | `add_responsive_control()` with `DIMENSIONS` | TAB_STYLE |463| **Margin** | `add_responsive_control()` with `DIMENSIONS` | TAB_STYLE |464| **Width / Height** | `add_responsive_control()` with `SLIDER` | TAB_STYLE |465| **Spacing / Gap** | `add_responsive_control()` with `SLIDER` | TAB_STYLE |466| **Image** | `MEDIA` + `add_group_control( Group_Control_Image_Size::get_type() )` | TAB_CONTENT |467| **CSS Filters** (if image/element) | `add_group_control( Group_Control_Css_Filter::get_type() )` | TAB_STYLE |468| **Hover state** | Duplicate color/background/shadow controls inside `'section_style_hover'` with `selectors` targeting `:hover` | TAB_STYLE |469| **Transition duration** | `SLIDER` (seconds) with `selectors => ['transition-duration']` | TAB_STYLE |470| **Link** | `URL` control with `'dynamic' => ['active' => true]` | TAB_CONTENT |471| **Icon** | `ICONS` control with `fa4compatibility` | TAB_CONTENT |472| **HTML tag** | `SELECT` (h1–h6, div, span, p) | TAB_CONTENT |473474### Example — Correct vs Incorrect475476```php477// ❌ WRONG — hardcoded color and font-size478protected function render(): void {479 $settings = $this->get_settings_for_display();480 echo '<h2 style="color: #e94560; font-size: 24px;">'481 . esc_html( $settings['title'] ) . '</h2>';482}483484// ✅ CORRECT — all visuals controlled from the panel via selectors485protected function register_controls(): void {486 // ... Content section with title TEXT control ...487488 $this->start_controls_section( 'section_title_style', [489 'label' => esc_html__( 'Title Style', 'myplugin' ),490 'tab' => \Elementor\Controls_Manager::TAB_STYLE,491 ] );492493 $this->add_control( 'title_color', [494 'label' => esc_html__( 'Color', 'myplugin' ),495 'type' => \Elementor\Controls_Manager::COLOR,496 'selectors' => [497 '{{WRAPPER}} .myplugin-widget__title' => 'color: {{VALUE}};',498 ],499 ] );500501 $this->add_group_control(502 \Elementor\Group_Control_Typography::get_type(),503 [504 'name' => 'title_typography',505 'selector' => '{{WRAPPER}} .myplugin-widget__title',506 ]507 );508509 $this->add_responsive_control( 'title_align', [510 'label' => esc_html__( 'Alignment', 'myplugin' ),511 'type' => \Elementor\Controls_Manager::CHOOSE,512 'options' => [513 'left' => [ 'title' => esc_html__( 'Left', 'myplugin' ), 'icon' => 'eicon-text-align-left' ],514 'center' => [ 'title' => esc_html__( 'Center', 'myplugin' ), 'icon' => 'eicon-text-align-center' ],515 'right' => [ 'title' => esc_html__( 'Right', 'myplugin' ), 'icon' => 'eicon-text-align-right' ],516 ],517 'selectors' => [518 '{{WRAPPER}} .myplugin-widget__title' => 'text-align: {{VALUE}};',519 ],520 ] );521522 $this->add_responsive_control( 'title_spacing', [523 'label' => esc_html__( 'Bottom Spacing', 'myplugin' ),524 'type' => \Elementor\Controls_Manager::SLIDER,525 'size_units' => [ 'px', 'em', 'rem' ],526 'range' => [ 'px' => [ 'min' => 0, 'max' => 100 ] ],527 'selectors' => [528 '{{WRAPPER}} .myplugin-widget__title' => 'margin-bottom: {{SIZE}}{{UNIT}};',529 ],530 ] );531532 $this->end_controls_section();533}534535protected function render(): void {536 $settings = $this->get_settings_for_display();537 // ✅ No inline styles — all visuals come from Elementor's selectors538 $this->add_render_attribute( 'title', 'class', 'myplugin-widget__title' );539 $this->add_inline_editing_attributes( 'title' );540 echo '<h2 ' . $this->get_render_attribute_string( 'title' ) . '>'541 . esc_html( $settings['title'] ) . '</h2>';542}543```544545### `add_render_attribute()` and `add_inline_editing_attributes()` — Always Use546547The official Elementor API for building HTML attributes is `$this->add_render_attribute()`.548**Always use it** instead of manually concatenating class/id/aria attributes in `render()`.549Pair with `$this->add_inline_editing_attributes()` for any text field that supports live550editing in the Elementor editor panel.551552**Outputting the built attributes — pick by context:** use553`get_render_attribute_string( 'key' )` when you are concatenating into a string (as in the554`echo '<h2 ' . ... . '>'` example above), and `print_render_attribute_string( 'key' )` when555you are echoing directly inside a `?> … <?php` HTML block (e.g.556`<h2 <?php $this->print_render_attribute_string( 'title' ); ?>>`). Both are correct Elementor557APIs — `print_*` simply echoes what `get_*` returns. The widget sub-files use the `print_*`558form inside their HTML templates.559560Source: developers.elementor.com/docs/widgets/rendering-html-attribute/561Source: developers.elementor.com/docs/widgets/rendering-inline-editing/