Elementor: Dynamic Tag types, fields & fallback

SkillAI & models

Build the body of an Elementor Dynamic Tag — choose Tag

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: Dynamic Tag types, fields & fallback skill

What this skill tells your AI

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

The body of a dynamic tag has three decisions: which base class (Tag vs Data_Tag), which categories it produces (so the right controls accept it), and which settings fields the editor shows. Plus the easily-missed fallback asymmetry between the two base classes. This skill assumes the tag is already being registered — see elementor-dynamic-tag-register for that.

Decision 1 — Tag vs Data_Tag

TagData_Tag
Extend\Elementor\Core\DynamicTags\Tag\Elementor\Core\DynamicTags\Data_Tag
You implementrender()echo the outputget_value( array $options = [] )return the value
get_content_type()'ui' (final)'plain' (final)
Use forrendered text/HTML fragments (price, reading time, a badge)structured values consumed by a control — an image array [ 'id' => …, 'url' => … ], a URL string, a color

Verified: Tag::get_content() does ob_start(); $this->render(); $value = ob_get_clean(); (tag.php:30-37) and get_content_type() returns 'ui' (tag.php:64). Data_Tag declares abstract protected function get_value() (data-tag.php:25), returns it directly from get_content() (data-tag.php:43-45), and get_content_type() returns 'plain' (data-tag.php:31).

Rule of thumb: if the value feeds a MEDIA / IMAGE / URL / COLOR control (the control needs a structured value, not printed markup), use Data_Tag. If it feeds a TEXT context (it's printed inline), use Tag.

// Tag — echoes
class Reading_Time extends Tag {
    public function render(): void {
        echo esc_html( $this->compute() . ' min' );   // print, don't return
    }
}

// Data_Tag — returns
class Featured_Image_Fallback extends Data_Tag {
    protected function get_value( array $options = [] ) {
        $id = get_post_thumbnail_id();
        if ( $id ) {
            return [ 'id' => $id, 'url' => wp_get_attachment_image_src( $id, 'full' )[0] ];
        }
        return $this->get_settings( 'fallback' );   // see "Fallback" below
    }
}

Decision 2 — categories (what value the tag produces)

get_categories() returns one or more category constants from Elementor\Modules\DynamicTags\Module. Categories declare the kind of value the tag emits; a control accepts a tag only when their categories overlap, so the editor shows your tag only under compatible controls.

Verified constants (modules/dynamic-tags/module.php:31-76):

ConstantValueTypical use
TEXT_CATEGORY'text'printed strings (most Tags)
URL_CATEGORY'url'link fields
IMAGE_CATEGORY'image'image controls
MEDIA_CATEGORY'media'media (image/video) controls
POST_META_CATEGORY'post_meta'meta-field contexts
GALLERY_CATEGORY'gallery'gallery controls
NUMBER_CATEGORY'number'number controls
COLOR_CATEGORY'color'color controls
DATETIME_CATEGORY'datetime'date/time controls
SVG_CATEGORY'svg'inline-SVG / icon controls

A tag may declare several — Pro's Post_Custom_Field returns [ TEXT, URL, POST_META, COLOR, DATETIME, MEDIA ] because a meta value can drive any of those controls. Reference the constant, never the bare string, so a renamed value can't break you.

use Elementor\Modules\DynamicTags\Module as TagsModule;

public function get_categories(): array {
    return [ TagsModule::TEXT_CATEGORY ];
}

Decision 3 — settings fields via register_controls()

Add the tag's configuration fields in register_controls(). Elementor has already opened a "Settings" controls section around your call (base-tag.php:175-187) — add controls directly; do not wrap them in your own start_controls_section().

use Elementor\Controls_Manager;

protected function register_controls(): void {
    $this->add_control( 'format', [
        'label'   => esc_html__( 'Format', 'myplugin' ),
        'type'    => Controls_Manager::SELECT,
        'default' => 'minutes',
        'options' => [
            'minutes' => esc_html__( 'Minutes', 'myplugin' ),
            'words'   => esc_html__( 'Word count', 'myplugin' ),
        ],
    ] );

    $this->add_control( 'wpm', [
        'label'   => esc_html__( 'Words / minute', 'myplugin' ),
        'type'    => Controls_Manager::NUMBER,
        'default' => 200,
        'min'     => 50,
    ] );
}

Common control types (from Elementor\Controls_Manager, all seen in verified tags/widgets): TEXT, TEXTAREA, NUMBER, SELECT, SELECT2 (add 'multiple' => true for multi), SWITCHER, CHOOSE, COLOR, MEDIA, ICONS, ALERT, REPEATER. Read settings back with $this->get_settings( 'key' ) (raw) or $this->get_settings_for_display() (parsed). For a large-dataset picker (products/posts) use the AJAX query control — see elementor-dynamic-tag-ajax-select; a plain SELECT2 preloaded with thousands of options freezes the editor.

Name your method register_controls(), not _register_controls(). The underscore form is deprecated since 3.1.0 — init_controls() calls it but emits a _doing_it_wrong notice (base-tag.php:179-182). (The reference plugin used the underscore form until its 2026-06-17 migration; older copies and tutorials still show it, so recognise it but don't copy it.)

Two optional panel hints on Base_Tag:

  • is_settings_required() — return true if the tag is useless until configured (Pro's Post_Custom_Field does). Default false (base-tag.php:83).
  • get_panel_template_setting_key() — return a control key to surface in the tag's panel label (e.g. 'key'). Default '' (base-tag.php:75).

The fallback system (the asymmetry that bites)

A Tag gets Before / After / Fallback controls for free and applies the fallback automatically when render() produces empty output. A Data_Tag gets none of this and must do it by hand.

Tag — automatic

Tag::register_advanced_section() adds an "Advanced" section with before, after, and fallback controls (tag.php:84-123), and get_content() applies them: if the rendered value is non-empty it prepends before / appends after; else if a fallback is set it uses wp_kses_post_deep( $settings['fallback'] ) (tag.php:39-55). You write render() and get fallback behaviour automatically — just make sure render() outputs nothing when there's no value (don't echo '0', '—', or an empty wrapper, or the fallback never triggers).

public function render(): void {
    $value = $this->compute();
    if ( '' === $value ) {
        return;   // emit nothing → Elementor's Fallback control takes over
    }
    echo esc_html( $value );
}

Data_Tag — manual

Data_Tag inherits the empty Base_Tag::register_advanced_section() (base-tag.php:166) — no Before/After/Fallback section is added, and get_content() does no fallback logic. To support a fallback you register the control yourself and consult it in get_value(). Pro's Post_Featured_Image is the canonical pattern:

protected function register_controls(): void {
    $this->add_control( 'fallback', [
        'label' => esc_html__( 'Fallback', 'myplugin' ),
        'type'  => Controls_Manager::MEDIA,   // match the control type your value feeds
    ] );
}

protected function get_value( array $options = [] ) {
    $id = get_post_thumbnail_id();
    if ( $id ) {
        return [ 'id' => $id, 'url' => wp_get_attachment_image_src( $id, 'full' )[0] ];
    }
    return $this->get_settings( 'fallback' );   // the manual fallback
}

Verified at post-featured-image.php:33-56get_value() returns the image array or $this->get_settings( 'fallback' ), and register_controls() adds a single MEDIA fallback control.

Critical rules

  • Tag echoes (render()); Data_Tag returns (get_value()). Mixing them up (returning from render(), or echoing from get_value()) silently produces empty/garbage output.
  • get_categories() returns Module::*_CATEGORY constants, never bare strings, and matches the control kinds your value feeds. Wrong categories → the tag never appears under the intended control.
  • Add controls directly in register_controls() — Elementor already opened the "Settings" section. A self-opened section nests incorrectly.
  • Method is register_controls(), not _register_controls() (deprecated 3.1.0).
  • Tag fallback is automatic but only triggers on empty output — render() must emit nothing when there's no value.
  • Data_Tag has no automatic fallback/before/after — register a fallback control and read it in get_value() yourself, with a type matching the consuming control.
  • Escape on output in Tag::render() (esc_html / wp_kses_post) — it's echoed into the page like any front-end output.

Common mistakes

// WRONG — Data_Tag that echoes (output is captured nowhere; control gets null)
class My_Image extends Data_Tag {
    protected function get_value( array $options = [] ) {
        echo wp_get_attachment_url( $id );   // <-- should RETURN
    }
}

// WRONG — Tag that returns (nothing is printed; tag renders empty)
class My_Text extends Tag {
    public function render() {
        return get_the_title();   // <-- should ECHO
    }
}

// WRONG — render() prints a placeholder, so Elementor's Fallback never fires
public function render() {
    $v = $this->compute();
    echo esc_html( $v ?: '—' );   // <-- non-empty → fallback control is dead
}
// RIGHT — emit nothing on empty
public function render() {
    $v = $this->compute();
    if ( '' === $v ) { return; }
    echo esc_html( $v );
}

// WRONG — Data_Tag expecting an automatic Fallback control (there is none)
class My_Url extends Data_Tag {
    protected function get_value( array $o = [] ) {
        return $this->get_settings( 'fallback' );   // <-- 'fallback' was never registered
    }
}
// RIGHT — register it first
protected function register_controls() {
    $this->add_control( 'fallback', [ 'type' => Controls_Manager::URL ] );
}

// WRONG — wrapping controls in your own section
protected function register_controls() {
    $this->start_controls_section( 'sec', [ 'label' => 'X' ] );  // <-- already inside "Settings"
    $this->add_control( /* … */ );
    $this->end_controls_section();
}

Cross-references

  • Run elementor-dynamic-tag-register for registering the tag, groups, hooks, and bootstrap timing.
  • Run elementor-dynamic-tag-ajax-select when a settings field must pick from a large dataset without a preloaded SELECT2.
  • See reference.md in this skill folder for the full control-type catalog and a complete worked Data_Tag.

What this skill does NOT cover

  • Registering the tag / groups / hookselementor-dynamic-tag-register.
  • AJAX/search option fieldselementor-dynamic-tag-ajax-select.
  • Group-control families (Typography, Box Shadow, etc.) — those are for widgets, not tag settings.
  • The full Controls_Manager control reference — see Elementor's controls docs; this skill cites only the types proven in dynamic-tag source.

References

Signals

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