You can build a working custom Elementor widget with two small PHP files: a plugin file that registers the widget on the elementor/widgets/register hook, and a widget class that extends \Elementor\Widget_Base. The class needs four short methods to describe the widget, one method to add controls to the editor panel, and one method to output the HTML. In this guide we build a reusable notice box widget in 48 lines of PHP in total, then look at how to extend it.
Why build your own widget?
Elementor's built-in widgets cover most layouts, but there are good reasons to make your own:
- Consistency. A client can drop in a branded component without touching spacing, colours or fonts, so pages stay on brand.
- Simplicity for editors. Instead of a stack of nested containers, the editor sees one widget with two or three fields.
- Fewer add-on plugins. Rather than installing a large widget pack for one feature, you write exactly what you need.
You need a WordPress site with Elementor installed (the free version is fine), access to the wp-content/plugins folder via FTP, SFTP or your host's file manager, and a code editor. Please work on a staging or local site, not on a live client site.
Step 1: create the plugin folder
Inside wp-content/plugins, create this structure:
b2c-notice-widget/
b2c-notice-widget.php
widgets/
notice-widget.phpKeeping widgets in their own folder makes it easy to add more later.
Step 2: the main plugin file
Put this in b2c-notice-widget.php:
<?php
/**
* Plugin Name: B2C Notice Widget
* Description: Adds a simple notice box widget to Elementor.
* Version: 1.0.0
*/
defined( 'ABSPATH' ) || exit;
function b2c_register_notice_widget( $widgets_manager ) {
require_once __DIR__ . '/widgets/notice-widget.php';
$widgets_manager->register( new B2C_Notice_Widget() );
}
add_action( 'elementor/widgets/register', 'b2c_register_notice_widget' );What each part does:
- The comment block at the top is the plugin header. WordPress reads it to list the plugin on the Plugins screen.
- The
defined( 'ABSPATH' ) || exit;line stops anyone loading the file directly in a browser. - The function runs only when Elementor fires
elementor/widgets/register. Because Elementor is fully loaded at that point, itsWidget_Baseclass exists and our class can extend it safely. $widgets_manager->register()adds our widget to the editor panel.
If you follow older tutorials, you may see elementor/widgets/widgets_registered and register_widget_type(). Those were deprecated in favour of the hook and method above, so use the newer versions for anything you build today and check the Elementor developer documentation if something has changed since the time of writing (October 2026).
Step 3: the widget class
Put this in widgets/notice-widget.php:
<?php
defined( 'ABSPATH' ) || exit;
class B2C_Notice_Widget extends \Elementor\Widget_Base {
public function get_name() { return 'b2c_notice'; }
public function get_title() { return 'Notice Box'; }
public function get_icon() { return 'eicon-info-box'; }
public function get_categories() { return array( 'general' ); }
protected function register_controls() {
$this->start_controls_section( 'content', array( 'label' => 'Content' ) );
$this->add_control( 'heading', array(
'label' => 'Heading',
'type' => \Elementor\Controls_Manager::TEXT,
'default' => 'Please note',
) );
$this->add_control( 'message', array(
'label' => 'Message',
'type' => \Elementor\Controls_Manager::TEXTAREA,
'default' => 'Our office is closed on bank holidays.',
) );
$this->end_controls_section();
}
protected function render() {
$s = $this->get_settings_for_display();
printf(
'<div class="b2c-notice"><strong>%s</strong><p>%s</p></div>',
esc_html( $s['heading'] ),
esc_html( $s['message'] )
);
}
}The four description methods
get_name(): a unique machine name. Prefix it so it never clashes with another plugin.get_title(): the label editors see in the panel.get_icon(): an Elementor icon class. Names startingeicon-come from Elementor's own icon font.get_categories(): which panel section the widget appears in.generalis the standard one.
register_controls()
Controls are the fields in the left-hand editor panel. You open a section, add controls, then close the section. Each control has an ID (here heading and message), a label, a type and an optional default. Elementor saves whatever the editor types under that ID.
render()
get_settings_for_display() returns the saved values as an array. We then print the HTML. Note the esc_html() calls: the content comes from a user, so we escape it on output, exactly as you would in any other WordPress code. Skipping this is how a harmless-looking widget becomes a security hole.
Step 4: activate and test
- Go to Plugins in the WordPress dashboard and activate B2C Notice Widget.
- Edit any page with Elementor.
- Search the widget panel for "Notice Box".
- Drag it onto the page, change the heading and message, and check the preview updates.
- Update the page and view it on the front end.
If the widget does not appear, check that Elementor is active, that the file paths match exactly, and look in your PHP error log for a typo. Enabling WP_DEBUG on your staging site will show errors on screen.
Step 5: give it some style
The widget outputs plain HTML with a b2c-notice class. You can style it from your theme or child theme's stylesheet, or let editors control the styling with a Style tab. Add this inside register_controls(), after the content section:
$this->start_controls_section( 'style', array(
'label' => 'Style',
'tab' => \Elementor\Controls_Manager::TAB_STYLE,
) );
$this->add_control( 'bg_colour', array(
'label' => 'Background colour',
'type' => \Elementor\Controls_Manager::COLOR,
'selectors' => array(
'{{WRAPPER}} .b2c-notice' => 'background-color: {{VALUE}};',
),
) );
$this->end_controls_section();The selectors array is the clever part. {{WRAPPER}} is replaced with a unique class for that widget instance, and {{VALUE}} with the chosen colour. Elementor writes the CSS for you, so each notice on the page can have its own colour without any extra PHP.
Ideas for extending it
- A select control (
Controls_Manager::SELECT) to choose between info, warning and success styles, output as a modifier class. - A URL control for an optional "Read more" link. Escape it with
esc_url(). - Typography and border group controls, which give editors the same styling options as Elementor's own widgets.
- A
content_template()method that renders the widget in the editor with JavaScript, so previews update instantly without a server round trip. This is optional: without it, Elementor falls back to the PHP render. - A custom widget category so all your agency's widgets sit together in the panel.
Common mistakes
- Extending
Widget_Basebefore Elementor loads. If you define the class at the top level of your main plugin file, you can get a "class not found" fatal error. Loading it inside the register hook avoids that. - Echoing unescaped settings. Always escape on output.
- Changing a control ID after launch. Existing pages store values by ID, so renaming
messagetotextleaves old widgets empty. - Generic names. A widget called
noticemay clash with another add-on. Prefix everything.
Once you have built one widget, the pattern is the same for every other: describe it, add controls, render it. In my own projects, a handful of small custom widgets often replaces a heavy add-on pack and keeps pages lighter. The first WordPress plugin course goes deeper into plugin structure, which makes widgets like this much easier to maintain.
Questions people ask
Do I need Elementor Pro to create custom widgets?
No. The widget API is part of the free Elementor plugin, so custom widgets work with or without Pro. Some Pro-only features, such as dynamic tags, only apply if Pro is installed.
Can I add a custom widget through my theme's functions.php instead of a plugin?
It works, but the widget disappears if the theme changes, and any page using it loses that content. A small plugin keeps the widget independent of the theme, which is better for client sites.
What is the difference between render and content_template?
render() outputs the widget with PHP and is used on the front end and in the editor. content_template() is an optional JavaScript template that lets the editor preview update instantly as you type. If you skip it, Elementor uses the PHP version for the preview.