You have the markup already. What you do not have is the two hundred lines of Elementor boilerplate around it, and the decision about which parts of it the editor should be able to change.
This makes that decision and writes the class. Text a person wrote becomes a control. An image becomes a MEDIA control, with its alt text as its own field. A link’s target becomes a URL control. Everything structural, the classes, the wrappers, the widths, stays in the markup where it belongs.
How to use
- Paste the HTML. A card, a hero, a call to action: one component, not a page.
- Name the widget. Prefix the class: Elementor loads every add-on into the same
global namespace, so
Feature_Cardwill collide with somebody eventually. - Copy the class into your add-on’s
widgets/folder and register it. The Elementor add-on plugin generator on this site writes that add-on. - Read the control list in the panel and move anything that is really styling to the Style tab.
Example
This HTML:
<div class="feature-card">
<img src="/wp-content/uploads/icon.png" alt="A small icon" width="64">
<h3>What this card is for</h3>
<p>A sentence or two of body copy.</p>
<a href="/tools/" class="feature-card__link">Read more</a>
</div>
becomes six controls and this render method:
protected function render() {
$settings = $this->get_settings_for_display();
?>
<div class="feature-card">
<img src="<?php echo esc_url( $settings['image']['url'] ); ?>" alt="<?php echo esc_attr( $settings['image_alt'] ); ?>" width="64" />
<h3>
<?php echo esc_html( $settings['h3_text'] ); ?>
</h3>
<p>
<?php echo esc_html( $settings['p_text'] ); ?>
</p>
<a href="<?php echo esc_url( $settings['link']['url'] ); ?>" class="feature-card__link">
<?php echo esc_html( $settings['a_text'] ); ?>
</a>
</div>
<?php
}
Three details worth noticing.
The escaping matches where the value lands: esc_html for text, esc_attr for
an attribute, esc_url for a URL. Using esc_html on a URL is the mistake that
turns & into & in a query string.
The class and width attributes stayed. They are not content, and a control
for every attribute is a widget nobody can use.
Text over eighty characters becomes a TEXTAREA rather than a TEXT control, because a paragraph in a single line input is miserable to edit.
Pitfalls
Move the styling to the Style tab yourself. The converter puts every control
on the Content tab, which is right for text and images and wrong for colours and
spacing. A COLOR or SLIDER control belongs on the Style tab with a selectors
array so Elementor writes the CSS. The widget builder on this site writes that
shape.
<script>, <iframe> and form elements are dropped. A widget cannot carry
them: Elementor saves its content through the same sanitising WordPress applies
elsewhere. Put scripts in the add-on’s own enqueue.
One component at a time. Paste a whole page and you get one widget with forty controls, which is worse than four widgets with five each. Convert the parts.
The editor preview is a server render. No content_template() is written, so
Elementor falls back to rendering on the server, which is correct but slower to
update in the editor. Add one later if the widget is used heavily.
Repeated markup wants a repeater, not numbered controls. Three cards in the
HTML become h3_text, h3_text_2, h3_text_3. That works, and a REPEATER
control is the better shape. Convert one card and add the repeater by hand.
Compatibility
The output targets Elementor 3, where register_controls is the protected method
name and \Elementor\Controls_Manager is the class. Elementor 2 used
_register_controls; if you are still there, rename the method and it works.
Control types used: TEXT, TEXTAREA, MEDIA and URL, all present since Elementor 1.
Utils::get_placeholder_image_src() is the placeholder for an image control with
no default, which is what Elementor’s own widgets use.
The PHP is WordPress coding standards shaped: tabs, array(), spaces inside
parentheses, and a docblock per method. Every variant of the output was run
through php -l.
Frequently asked questions
Where do I put the class?
widgets/class-feature-card.php,
loaded on Elementor’s elementor/widgets/register action. The add-on generator on
this site writes the plugin with that hook already in place.