Skip to content
wp-skills
Register
tools/elementor-widget-generator

Elementor Widget Generator

Build an Elementor widget class and the few lines that register it: content controls the template reads, style controls Elementor turns into CSS, responsive values, typography, and a panel category of your own — on the hooks Elementor actually documents today.

The widget

What the editor's widget panel shows. A widget that leaves it out registers fine and appears with no name at all.

What get_name() returns. Saved into every page that uses the widget, so changing it later orphans them.

Any Elementor icon class. The default for a widget that does not say is eicon-apps.

The class the markup carries and every style control's selector targets. Blank derives it from the widget name.

Comma separated. What the panel's search matches on besides the title.

Panel category

A widget naming a category nobody registered is not an error — it registers, belongs to no section, and never appears in the panel.

Sections Elementor registers itself.

Controls

One call, ten controls — family, size, weight, transform, style, decoration, line height and spacing — all targeting the wrapper.

Controls2
  • #1

    The key the settings array is read by, and what is stored in the page.

  • #2

    The key the settings array is read by, and what is stored in the page.

    Written into selectors, which is what puts the control on the Style tab as far as Elementor is concerned.

    Emits add_responsive_control, so desktop, tablet and mobile each get their own value.

widgets/class-my-widget-widget.php
<?php
/**
 * Elementor widget: My Widget
 *
 * Generated by wp-skills.com
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * My Widget.
 *
 * Only get_name() is abstract on \Elementor\Widget_Base — it is declared on
 * Controls_Stack, and everything else below has a working implementation
 * upstream. Verified against Elementor 4.2.4, where a subclass declaring
 * nothing but get_name() registered successfully and came back with an empty
 * title, the icon "eicon-apps" and the category "general". An empty title is
 * why get_title() is here regardless: the widget would otherwise sit nameless
 * in the panel.
 */
class My_Widget_Widget extends \Elementor\Widget_Base {

	/**
	 * The key Elementor stores this widget under, and the value saved into
	 * every page that uses it. Changing it later orphans those pages.
	 */
	public function get_name(): string {
		return 'my_widget';
	}

	public function get_title(): string {
		return __( 'My Widget', 'text-domain' );
	}

	/**
	 * Any Elementor icon class. The default for a widget that does not say is
	 * "eicon-apps".
	 */
	public function get_icon(): string {
		return 'eicon-post-content';
	}

	/**
	 * The panel section this widget appears under.
	 *
	 * A slug nobody registered is not an error — the widget still registers,
	 * and its config still carries the slug. It simply belongs to no section,
	 * so nothing in the editor shows it. Measured in 4.2.4.
	 */
	public function get_categories(): array {
		return array( 'general' );
	}

	/**
	 * register_controls(), without the leading underscore.
	 *
	 * _register_controls() still exists and still runs — Elementor deprecated
	 * it in 3.1.0 rather than removing it, and Controls_Stack::init_controls()
	 * calls whichever one the subclass defined, raising a deprecation notice
	 * for the old name. Verified in 4.2.4.
	 *
	 * There is no Advanced tab here on purpose: Elementor adds it itself. A
	 * widget declaring no controls at all already carries 166 of them —
	 * measured — covering margin, padding, positioning, motion effects,
	 * responsive visibility and custom CSS.
	 */
	protected function register_controls(): void {
		$this->start_controls_section(
			'section_content',
			array(
				'label' => __( 'Content', 'text-domain' ),
				'tab'   => \Elementor\Controls_Manager::TAB_CONTENT,
			)
		);
		$this->add_control(
			'heading',
			array(
				'label'   => __( 'Heading', 'text-domain' ),
				'type'    => \Elementor\Controls_Manager::TEXT,
				'default' => __( 'Hello', 'text-domain' ),
			)
		);

		$this->end_controls_section();

		$this->start_controls_section(
			'section_style',
			array(
				'label' => __( 'Style', 'text-domain' ),
				'tab'   => \Elementor\Controls_Manager::TAB_STYLE,
			)
		);

		$this->add_control(
			'text_colour',
			array(
				'label'     => __( 'Text colour', 'text-domain' ),
				'type'      => \Elementor\Controls_Manager::COLOR,
				'selectors' => array(
					'{{WRAPPER}} .my-widget' => 'color: {{VALUE}};',
				),
			)
		);

		$this->add_group_control(
			\Elementor\Group_Control_Typography::get_type(),
			array(
				'name'     => 'my_widget_typography',
				'selector' => '{{WRAPPER}} .my-widget',
			)
		);

		$this->end_controls_section();
	}

	/**
	 * The front end.
	 *
	 * get_settings_for_display() rather than get_settings(): it is the one that
	 * resolves dynamic tags and the responsive value for the current device.
	 */
	protected function render(): void {
		$settings = $this->get_settings_for_display();
		?>
		<div class="my-widget">
			<p class="my-widget__heading"><?php echo esc_html( $settings['heading'] ); ?></p>
		</div>
		<?php
	}
}