Elementor: experiments and markup-changing features

SkillMedia

Design Elementor addons / widgets / dynamic tags to survive

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Elementor: experiments and markup-changing features skill

What this skill tells your AI

The instructions your AI receives, as published by lonsdale201/wp-agent-skills in elementor/elementor-experiments-and-markup/SKILL.md and read by ahel’s review.

For developers shipping a companion plugin / theme that adds widgets, dynamic tags, or frontend CSS/JS on top of Elementor. Elementor ships "experiments" (feature flags) under core/experiments/manager.php. Most are cosmetic, but two Performance experiments change the rendered HTML and are default-ON on new installs, so a modern addon must handle them or it silently breaks on a large share of sites.

Read this first — you cannot assume the state

Experiment state is per-site and three-valued (Default / Active / Inactive), and the effective default differs between fresh and upgraded installs. Never hardcode an assumption; detect at runtime:

if ( \Elementor\Plugin::$instance->experiments->is_feature_active( 'e_optimized_markup' ) ) {
    // adapt markup
}

Verified: Experiments\Manager::is_feature_active( $name, $check_dependencies = false ) at core/experiments/manager.php:257; actual state resolves to the user's explicit state unless it's Default, then the feature's default (manager.php get_feature_actual_state).

The two markup-changing experiments (both release_status => STABLE, tag "Performance", new_site.default_active => true) — verified manager.php:312-381:

ExperimentnameDefault on NEW install (≥ ver)What it changes
Inline Font Iconse_font_icon_svgON (≥ 3.17.0)Icons render as inline <svg>; Font Awesome + eicons CSS/fonts NOT loaded on frontend
Optimized Markupe_optimized_markupON (≥ 3.30.0)Removes inner wrapper HTML (.elementor-widget-container) from widgets to shrink the DOM

new_site.default_active => true means a site first installed at/after that version defaults the experiment ON. Sites upgraded from older versions may keep it OFF (e_optimized_markup / container have default => STATE_INACTIVE). So the SAME addon meets both states in the wild — which is exactly why you detect at runtime.

Misconception this skill corrects

"My widget's icons render fine in the Elementor editor, so my icon markup is correct."

The editor/preview is misleading. Icons_Manager only switches to inline SVG when not in edit/preview mode — verified includes/managers/icons.php:186 (is_font_icon_inline_svg() && ! is_edit_mode() && ! is_preview_mode()). So with Inline Font Icons ON, the editor still loads the icon fonts and hardcoded <i class="fas fa-star"> looks correct there — but on the frontend the Font Awesome CSS is absent and your hardcoded icon renders as an empty/broken glyph. Always test on the published frontend, and always emit icons through Icons_Manager::render_icon().

When to use this skill

  • Building or reviewing a custom widget (its render() markup, wrapper structure, or icon output).
  • Shipping frontend CSS/JS that targets Elementor's DOM (.elementor-widget-container, widget inner structure).
  • Any addon that renders icons chosen via the ICONS control.
  • Diagnosing "works on my site, breaks on the client's" icon/layout bugs — usually an experiment default difference.
  • The diff references is_feature_active, has_widget_inner_wrapper, Icons_Manager, e_optimized_markup, or e_font_icon_svg.

Optimized Markup (e_optimized_markup)

The wrapper contract lives on the element base. Element_Base::has_widget_inner_wrapper() returns true by default — verified includes/base/element-base.php:1588. Widget_Base::render() prints the <div class="elementor-widget-container"> only when that method returns true (includes/base/widget-base.php:427), and the widget's frontend script-dependency group flips commoncommon-optimized based on it (widget-base.php:201).

Core widgets opt IN to the DOM reduction by overriding it — the canonical one-liner, e.g. includes/widgets/button.php:85 and Pro's modules/forms/widgets/form.php:45:

public function has_widget_inner_wrapper(): bool {
    return ! \Elementor\Plugin::$instance->experiments->is_feature_active( 'e_optimized_markup' );
}

What this means for YOUR addon:

  • Your widget does NOT break by default. Since the base returns true, a widget that doesn't override the method keeps its .elementor-widget-container even when Optimized Markup is on. No action needed for correctness.
  • Your CSS/JS targeting CORE (and Pro) widgets DOES break when Optimized Markup is on, because those widgets drop the wrapper. A selector like .elementor-widget-heading .elementor-widget-container h2 {} stops matching; el.querySelector('.elementor-widget-container') returns null. Target the widget wrapper (.elementor-widget-{name} / .elementor-element) or the content element directly, not the inner container.
  • To let your widget participate in the DOM reduction, override has_widget_inner_wrapper() with the same one-liner — then make sure your own styles/scripts don't depend on .elementor-widget-container, and register a -optimized-aware style if needed.

Inline Font Icons (e_font_icon_svg)

When active on the frontend, Icons_Manager::render_font_icon() resolves the icon to an inline <svg> via the SVG data manager and returns it; otherwise it falls back to <i class="{icon value}"> which needs the font CSS — verified includes/managers/icons.php:309-341. The single rule:

// RIGHT — works in BOTH modes; emits <svg> when the experiment is on, <i> otherwise
\Elementor\Icons_Manager::render_icon(
    $settings['my_icon'],           // the ICONS control value: [ 'value' => ..., 'library' => ... ]
    [ 'aria-hidden' => 'true' ],
    'i'                             // wrapping tag; ignored for uploaded-SVG
);
  • render_icon() (icons.php:354) → get_icon_html() (icons.php:67) handles uploaded SVGs (library === 'svg') AND font icons transparently. Use it for every icon your widget outputs.
  • Do NOT hardcode <i class="fas fa-..."> or manually enqueue Font Awesome expecting it to be present — with the experiment on, Elementor deliberately does not load FA/eicons CSS on the frontend, so hardcoded markup renders broken.
  • If you genuinely need the font CSS (rare, legacy markup you can't convert), Icons_Manager::enqueue_shim() exists, but the right fix is to route through render_icon().
  • This experiment sets generator_tag => true, so its state is also observable in the <meta name="generator"> Elementor emits — handy for support triage.

Registering your OWN experiment (optional)

Addons can register experiments on the same system via the elementor/experiments/default-features-registered action — verified core/experiments/manager.php:433. Allowed add_feature() option keys (manager.php:1049): name, title, tag, tags, description, release_status, default, mutable, hidden, new_site, on_state_change, dependencies, generator_tag, messages, deprecated.

add_action( 'elementor/experiments/default-features-registered', function ( $experiments ): void {
    $experiments->add_feature( [
        'name'           => 'myplugin_new_renderer',
        'title'          => esc_html__( 'My Plugin: New Renderer', 'myplugin' ),
        'release_status' => \Elementor\Core\Experiments\Manager::RELEASE_STATUS_BETA,
        'default'        => \Elementor\Core\Experiments\Manager::STATE_INACTIVE,
    ] );
} );

// then gate behavior:
if ( \Elementor\Plugin::$instance->experiments->is_feature_active( 'myplugin_new_renderer' ) ) { /* ... */ }

Critical rules

  • Detect, never assume. Plugin::$instance->experiments->is_feature_active( $name ) — the effective default differs between fresh and upgraded installs.
  • Both markup experiments are STABLE and default-ON on new installs — treat "on" as the common case, not an edge case.
  • Icons: always Icons_Manager::render_icon(). Never hardcode <i class="fa">; the editor hides the breakage because it still loads fonts in edit/preview mode.
  • Optimized Markup: your widget keeps its wrapper by default (base returns true), so it won't break — but your CSS/JS against core/Pro widgets' .elementor-widget-container will. Don't target the inner container.
  • To opt your widget into DOM reduction, override has_widget_inner_wrapper() with the core one-liner, and make your own styles wrapper-independent.
  • Don't fight the experiment by force-enqueuing Font Awesome or re-adding wrappers globally — you reintroduce the DOM/asset cost the site owner opted out of and cause the exact third-party conflicts the experiment warns about.

Common mistakes

// WRONG — hardcoded font icon; broken on frontend when e_font_icon_svg is on
echo '<i class="fas fa-star"></i>';

// RIGHT — routes through the manager, SVG or font as appropriate
\Elementor\Icons_Manager::render_icon( $settings['star_icon'], [ 'aria-hidden' => 'true' ] );

// WRONG — CSS assumes the inner container always exists
// .my-addon .elementor-widget-button .elementor-widget-container { gap: 8px; }
//   → no match when Optimized Markup drops the wrapper on the core Button widget

// RIGHT — target the widget/element wrapper or the content node
// .my-addon .elementor-widget-button .elementor-button { gap: 8px; }

// WRONG — JS that expects the wrapper on a core widget
// const c = widgetEl.querySelector('.elementor-widget-container'); c.dataset.x = 1; // c is null

// RIGHT — guard / target a stable node
// const c = widgetEl.querySelector('.elementor-widget-container') || widgetEl;

// WRONG — assuming your custom widget lost its wrapper (it didn't; base returns true)
// ...adding a second wrapper "to be safe" → double nesting

// RIGHT — override only if you WANT the reduction, mirroring core
public function has_widget_inner_wrapper(): bool {
    return ! \Elementor\Plugin::$instance->experiments->is_feature_active( 'e_optimized_markup' );
}

Cross-references

  • Run elementor-dynamic-tag-register / elementor-dynamic-tag-fields when the work is registering a dynamic tag; if the tag outputs an icon, the Inline Font Icons rule here applies.
  • Run elementor-dynamic-tag-ajax-select for large-dataset controls (unrelated to markup, but same addon surface).
  • Run elementor-deprecations when a markup/API change you rely on may be deprecated — the has_widget_inner_wrapper / Icons_Manager APIs are current, but audit before bumping Elementor majors.

What this skill does NOT cover

  • Atomic Widgets / Editor V4 (e_atomic_elements, Pro AtomicWidgetsModule::EXPERIMENT_NAME, e_pro_atomic_form, collection-loop) — a much larger architectural shift (new widget base, styles engine, schema) that deserves its own skill. This skill is only the two STABLE markup-changing performance experiments plus the experiments mechanics.
  • Flexbox/Grid Container (container) beyond noting it's still an experiment — layout authoring, not addon-markup contract.
  • The editor-JS side of experiments (React panels, elementorCommon.config.experimentalFeatures).
  • Elementor's own internal widget CSS — you adapt to it, you don't edit it.

References

Signals

GitHub stars
22
Forks
2
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
elementor-experiments-and-markup
Source
github.com/lonsdale201/wp-agent-skills