Elementor: Dynamic Tag types, fields & fallback
SkillAI & modelsBuild the body of an Elementor Dynamic Tag — choose Tag
Available today. Use it from your connected AI after setup.
No other account needed.
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
Tag | Data_Tag | |
|---|---|---|
| Extend | \Elementor\Core\DynamicTags\Tag | \Elementor\Core\DynamicTags\Data_Tag |
| You implement | render() — echo the output | get_value( array $options = [] ) — return the value |
get_content_type() | 'ui' (final) | 'plain' (final) |
| Use for | rendered 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):
| Constant | Value | Typical 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()— returntrueif the tag is useless until configured (Pro'sPost_Custom_Fielddoes). Defaultfalse(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-56 — get_value() returns the image array or $this->get_settings( 'fallback' ), and register_controls() adds a single MEDIA fallback control.
Critical rules
Tagechoes (render());Data_Tagreturns (get_value()). Mixing them up (returning fromrender(), or echoing fromget_value()) silently produces empty/garbage output.get_categories()returnsModule::*_CATEGORYconstants, 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). Tagfallback is automatic but only triggers on empty output —render()must emit nothing when there's no value.Data_Taghas no automatic fallback/before/after — register afallbackcontrol and read it inget_value()yourself, with atypematching 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-registerfor registering the tag, groups, hooks, and bootstrap timing. - Run
elementor-dynamic-tag-ajax-selectwhen a settings field must pick from a large dataset without a preloadedSELECT2. - See
reference.mdin this skill folder for the full control-type catalog and a complete workedData_Tag.
What this skill does NOT cover
- Registering the tag / groups / hooks —
elementor-dynamic-tag-register. - AJAX/search option fields —
elementor-dynamic-tag-ajax-select. - Group-control families (Typography, Box Shadow, etc.) — those are for widgets, not tag settings.
- The full
Controls_Managercontrol reference — see Elementor's controls docs; this skill cites only the types proven in dynamic-tag source.
References
Tag: wp-content/plugins/elementor/core/dynamic-tags/tag.php —get_content()ob/render/fallback (30-58),'ui'type (64), advanced section with before/after/fallback (84-123).Data_Tag: wp-content/plugins/elementor/core/dynamic-tags/data-tag.php — abstractget_value()(25),'plain'type (31),get_content()returns value (43-45).Base_Tag: wp-content/plugins/elementor/core/dynamic-tags/base-tag.php —init_controls()opens Settings +_register_controlsdeprecation (172-187), emptyregister_advanced_section()(166),is_settings_required()(83),get_panel_template_setting_key()(75).- Categories: wp-content/plugins/elementor/modules/dynamic-tags/module.php:31-76.
- Manual-fallback Data_Tag: wp-content/plugins/elementor-pro/modules/dynamic-tags/tags/post-featured-image.php:33-56.
- Tag with SELECT + TEXT controls +
is_settings_required(): wp-content/plugins/elementor-pro/modules/dynamic-tags/tags/post-custom-field.php:46-105. - Official documentation: https://developers.elementor.com/docs/dynamic-tags/
- Official documentation: https://developers.elementor.com/docs/dynamic-tags/dynamic-tag-data/
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