HTML to Elementor Widget Converter

Paste a block of HTML and get an Elementor widget class, with a control for every piece of it a person would want to edit.

Enable JavaScript to convert your own HTML; the class below is for the example.

The markup you already have. Nothing is uploaded.

The label in the Elementor panel.

Prefix it. Elementor loads every add-on into the same global namespace.

Stored in every page that uses the widget, so changing it later orphans them. Left empty it is the title, slugged.

An Elementor icon class. eicon-code is the generic one.

Match the add-on plugin's own domain, or the labels will not translate.

6 controls: 1 MEDIA, 3 TEXT, 1 TEXTAREA, 1 URL.

  1. MEDIA image — /wp-content/uploads/icon.png
  2. TEXT image_alt — A small icon
  3. TEXT h3_text — What this card is for
  4. TEXTAREA p_text — A sentence or two of body copy, long enough that the editor
  5. URL link — /tools/
  6. TEXT a_text — Read more
class-widget.php
<?php
/**
 * The "Feature Card" Elementor widget, from pasted HTML.
 *
 * The controls were taken from the markup: text a person wrote became a
 * control, an image became MEDIA, a link target became URL, and the rest
 * stayed as markup. Move anything to the Style tab that belongs there.
 */

defined( 'ABSPATH' ) || exit;

class Bucket_Feature_Card extends \Elementor\Widget_Base {

	/**
	 * The name stored in every page that uses the widget.
	 *
	 * @return string
	 */
	public function get_name() {
		return 'feature-card';
	}

	/**
	 * The label in the panel.
	 *
	 * @return string
	 */
	public function get_title() {
		return esc_html__( 'Feature Card', 'bucket-addon' );
	}

	/**
	 * The panel icon.
	 *
	 * @return string
	 */
	public function get_icon() {
		return 'eicon-info-box';
	}

	/**
	 * Which panel category it appears in.
	 *
	 * @return array
	 */
	public function get_categories() {
		return array( 'general' );
	}

	/**
	 * The controls, one per piece of the markup a person would want to change.
	 */
	protected function register_controls() {
		$this->start_controls_section(
			'content',
			array(
				'label' => esc_html__( 'Content', 'bucket-addon' ),
				'tab'   => \Elementor\Controls_Manager::TAB_CONTENT,
			)
		);

		$this->add_control(
			'image',
			array(
				'label' => esc_html__( 'Image', 'bucket-addon' ),
				'type'  => \Elementor\Controls_Manager::MEDIA,
				'default' => array(
					'url' => '/wp-content/uploads/icon.png',
				),
			)
		);

		$this->add_control(
			'image_alt',
			array(
				'label' => esc_html__( 'Image Alt', 'bucket-addon' ),
				'type'  => \Elementor\Controls_Manager::TEXT,
				'default' => esc_html__( 'A small icon', 'bucket-addon' ),
				'label_block' => true,
			)
		);

		$this->add_control(
			'h3_text',
			array(
				'label' => esc_html__( 'H3 Text', 'bucket-addon' ),
				'type'  => \Elementor\Controls_Manager::TEXT,
				'default' => esc_html__( 'What this card is for', 'bucket-addon' ),
				'label_block' => true,
			)
		);

		$this->add_control(
			'p_text',
			array(
				'label' => esc_html__( 'P Text', 'bucket-addon' ),
				'type'  => \Elementor\Controls_Manager::TEXTAREA,
				'default' => esc_html__( 'A sentence or two of body copy, long enough that the editor will want a textarea rather than a single line.', 'bucket-addon' ),
				'label_block' => true,
				'rows' => 4,
			)
		);

		$this->add_control(
			'link',
			array(
				'label' => esc_html__( 'Link', 'bucket-addon' ),
				'type'  => \Elementor\Controls_Manager::URL,
				'default' => array(
					'url' => '/tools/',
				),
			)
		);

		$this->add_control(
			'a_text',
			array(
				'label' => esc_html__( 'A Text', 'bucket-addon' ),
				'type'  => \Elementor\Controls_Manager::TEXT,
				'default' => esc_html__( 'Read more', 'bucket-addon' ),
				'label_block' => true,
			)
		);

		$this->end_controls_section();
	}

	/**
	 * The front end. Everything a person can change is read from settings;
	 * everything else is the markup it came from.
	 */
	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" height="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
	}
}

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

  1. Paste the HTML. A card, a hero, a call to action: one component, not a page.
  2. Name the widget. Prefix the class: Elementor loads every add-on into the same global namespace, so Feature_Card will collide with somebody eventually.
  3. 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.
  4. 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 &amp; 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?
In the add-on plugin that registers it, usually 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.
Why is there a separate control for the alt text?
Because alt text is content, it is per-instance, and Elementor’s MEDIA control does not carry it. Leaving it hard-coded means every copy of the widget describes the first image.
Can it read my CSS too?
No. It reads the markup you paste and nothing else. The classes are kept, so your existing stylesheet still applies; enqueue it from the add-on.
What about a widget with no editable content?
You get the class with an empty controls section and a comment saying so, which is still most of the boilerplate. Add a control by hand or reconsider whether it needs to be a widget rather than a pattern.
Does it handle nested repeated items?
It numbers them, which works but is not the shape you want. For anything that repeats, convert a single item and wrap it in a REPEATER control: Elementor’s own docs show the pattern, and it is about fifteen lines.

From the people who built this tool

Master Addons for Elementor

The widgets Elementor leaves out, built to stay fast: advanced tabs and accordions, mega menus, tables, forms and the extensions around them.

See Master Addons Free version on WordPress.org

Weekly drops

New tools, when there are new tools

One email when something worth using ships. No schedule to fill, so no filler.

Your address goes nowhere else, and one click unsubscribes.