Elementor widget skeleton
A minimal custom Elementor widget with controls, escaped rendering and the registration hook, ready to extend.
WordPressHookselementorwidgetpage-builderoop
Code
<?php
/**
* A "Callout" widget for Elementor.
*
* @package Acme_Core
*/
namespace Acme\Core\Elementor;
use Elementor\Controls_Manager;
use Elementor\Widget_Base;
defined( 'ABSPATH' ) || exit;
class Callout_Widget extends Widget_Base {
public function get_name(): string {
return 'acme-callout';
}
public function get_title(): string {
return esc_html__( 'Callout', 'acme-core' );
}
public function get_icon(): string {
return 'eicon-info-box';
}
public function get_categories(): array {
return array( 'general' );
}
protected function register_controls(): void {
$this->start_controls_section( 'content', array(
'label' => esc_html__( 'Content', 'acme-core' ),
'tab' => Controls_Manager::TAB_CONTENT,
) );
$this->add_control( 'title', array(
'label' => esc_html__( 'Title', 'acme-core' ),
'type' => Controls_Manager::TEXT,
'default' => esc_html__( 'Heads up', 'acme-core' ),
) );
$this->add_control( 'tone', array(
'label' => esc_html__( 'Tone', 'acme-core' ),
'type' => Controls_Manager::SELECT,
'options' => array( 'info' => 'Info', 'warn' => 'Warning' ),
'default' => 'info',
) );
$this->add_control( 'link', array(
'label' => esc_html__( 'Link', 'acme-core' ),
'type' => Controls_Manager::URL,
) );
$this->end_controls_section();
}
protected function render(): void {
$s = $this->get_settings_for_display();
$tone = in_array( $s['tone'], array( 'info', 'warn' ), true ) ? $s['tone'] : 'info';
$this->add_render_attribute( 'wrap', 'class', array( 'acme-callout', 'acme-callout--' . $tone ) );
if ( ! empty( $s['link']['url'] ) ) {
$this->add_link_attributes( 'a', $s['link'] );
}
?>
<div <?php $this->print_render_attribute_string( 'wrap' ); ?>>
<strong><?php echo esc_html( $s['title'] ); ?></strong>
<?php if ( ! empty( $s['link']['url'] ) ) : ?>
<a <?php $this->print_render_attribute_string( 'a' ); ?>><?php esc_html_e( 'Read more', 'acme-core' ); ?></a>
<?php endif; ?>
</div>
<?php
}
}
// Registration (put in your plugin or theme bootstrap):
add_action( 'elementor/widgets/register', static function ( $widgets_manager ): void {
$widgets_manager->register( new Callout_Widget() );
} );
How it works
What it solves
Page builder users ask for reusable blocks of your design, but a widget class needs to be set up correctly or it breaks the editor. This skeleton shows the smallest widget that is still safe, translatable and idiomatic for current Elementor.
How it works
The class extends Widget_Base and declares a name, title, icon and category. register_controls() defines a content section with a text field, a select and a URL control. In render(), settings come from get_settings_for_display(), the tone is validated against an allow-list, and the output uses Elementor's render attribute helpers so link attributes such as rel and target are built correctly.
The widget is registered on elementor/widgets/register, the current hook.
Adapting it
Add a style section with typography and colour controls using the group control classes, and add an get_style_depends() method returning a registered stylesheet handle. Change the namespace and prefix for your project. For live editing previews, add a content_template() mirroring render().
Pitfalls
- Wrap registration so the file does nothing when Elementor is inactive; the hook simply never fires.
- Do not echo settings unescaped; builder content can be edited by many roles.
- Old tutorials use
widgets_registered, which is deprecated. - Control IDs become saved data, so rename them carefully.
How to use it
Place the class in your plugin or theme, load it from a bootstrap file, and keep it behind a check such as did_action( 'elementor/loaded' ).
Open the editor and search for Callout in the widget panel.
Extend by adding controls and matching output in render().
Licence: GPL-2.0-or-later. This snippet comes from WordPress code and is GPL-2.0-or-later.