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