Elementor is one of the most popular page builders for WordPress. It offers a wide range of pre-built widgets. But what if you need something unique that isn’t available out of the box? We’ll show you how to create a custom Elementor widget from scratch. By the end, you’ll be able to add fully personalized elements to your pages, tailored exactly to your needs.
The website you’re currently browsing is also built with Elementor. Many of its elements were created as custom widgets to meet specific design and functionality needs. For this tutorial, we’ll use a real-world example. Creating a custom “Plans” block, similar to the one displayed on our homepage. This will help you understand how to build a widget that is both visually appealing and fully functional within Elementor.

How to create the custom widget for Elementor?
To achieve this, we’ll use the special Elementor hook elementor/widgets/register. It helps create a custom function that runs when this hook is triggered. This function will be responsible for registering our new widget so that it appears in Elementor’s editor, ready to be added to any page.
add_action( 'elementor/widgets/register', 'register_custom_elementor_widget' );
Our function, register_custom_elementor_widget, receives the $widgets_manager object as a parameter. This object is where we register our new widget, making it available within Elementor’s interface. Essentially, $widgets_manager acts as the central manager for all Elementor widgets, and by adding our custom widget here, we integrate it seamlessly into the editor.
At this stage, our code has this look:
function register_custom_elementor_widget( $widgets_manager ) {
require_once( __DIR__ . '/plans/plans.php' );
$widgets_manager->register( new \WG_Plans_Widget() );
}
add_action( 'elementor/widgets/register', 'register_custom_elementor_widget' );
We’ll move the main widget code into a separate file called plans.php. Here we’ll define the WG_Plans_Widget class. This class will handle all the functionality and rendering for our custom “Plans” widget. It helps keep code organized and modular.
Optionally, we can create our own category for Elementor widgets and add our custom widgets to it. To do this, we’ll use the elementor/elements/categories_registered hook and implement a function called add_elementor_widget_categories. This function receives the $widgets_manager object as a parameter, which we’ll use to register our custom category. It keeps our widgets organized and easy to find in the Elementor editor.
function add_elementor_widget_categories( $widgets_manager ) {
$widgets_manager->add_category(
'webgarage',
[
'title' => __( 'WebGarage', 'text-domain' ),
'icon' => 'fa fa-sharp fa-regular fa-garage',
]
);
}
add_action( 'elementor/elements/categories_registered', 'add_elementor_widget_categories' );
Before running any of this code, we’ll perform a check to ensure Elementor is loaded by using:
if ( did_action( 'elementor/loaded' ) ) {
function register_custom_elementor_widget( $widgets_manager ) {
require_once( __DIR__ . '/plans/plans.php' );
$widgets_manager->register( new \WG_Plans_Widget() );
}
add_action( 'elementor/widgets/register', 'register_custom_elementor_widget' );
function add_elementor_widget_categories( $widgets_manager ) {
$widgets_manager->add_category(
'webgarage',
[
'title' => __( 'WebGarage', 'text-domain' ),
'icon' => 'fa fa-sharp fa-regular fa-garage',
]
);
}
add_action( 'elementor/elements/categories_registered', 'add_elementor_widget_categories' );
}
This ensures that our custom widget code only runs after Elementor has been fully initialized, preventing errors and conflicts.
Next, inside the plans.php file, we’ll create the actual object of our widget by defining the WG_Plans_Widget class. This class will extend \Elementor\Widget_Base and will include all the necessary methods to set the widget’s name, title, icon, category, and render its content in Elementor.
class WG_Plans_Widget extends \Elementor\Widget_Base {
public function get_name() {
return 'wg_plans_widget';
}
public function get_title() {
return __( 'Plans', 'plugin-name' );
}
public function get_icon() {
return 'eicon-elementor';
}
public function get_categories() {
return [ 'webgarage' ];
}
protected function _register_controls() {
$this->start_controls_section(
'content_section',
[
'label' => __( 'Content', 'plugin-name' ),
'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
]
);
$this->add_control(
'plans',
[
'label' => __( 'Plans', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::REPEATER,
'fields' => [
[
'name' => 'popular',
'label' => __( 'Popular', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::SWITCHER,
'label_on' => __( 'Yes', 'plugin-name' ),
'label_off' => __( 'No', 'plugin-name' ),
'return_value' => 'yes',
'default' => '',
],
[
'name' => 'title',
'label' => __( 'Title', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => __( 'Service Title', 'plugin-name' ),
'label_block' => true,
],
[
'name' => 'description',
'label' => __( 'Description', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::TEXTAREA,
],
[
'name' => 'price',
'label' => __( 'Price', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => '',
],
[
'name' => 'period',
'label' => __( 'Period', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => '',
],
[
'name' => 'info',
'label' => __( 'Info', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::REPEATER,
'fields' => [
[
'name' => 'item',
'label' => __( 'Item', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => '',
'label_block' => true,
],
],
'title_field' => '{{{ item }}}',
],
[
'name' => 'button_label',
'label' => __( 'Button Label', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => __( 'Learn More', 'plugin-name' ),
'label_block' => true,
],
[
'name' => 'button_link',
'label' => __( 'Button Link', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::URL,
'placeholder' => __( 'https://example.com', 'plugin-name' ),
],
],
'title_field' => '{{{ title }}}',
]
);
$this->end_controls_section();
}
protected function render() {
$settings = $this->get_settings_for_display();
?>
Next, I will describe in detail what each method of this class does.
public function get_name() {
return 'wg_plans_widget';
}
The get_name() method returns a unique identifier for the widget. This ID is used internally by Elementor to register and recognize the widget. It must be in lowercase, without spaces, and unique across all widgets on the site. Think of it as the widget’s internal “slug” that Elementor relies on to distinguish it from others.
public function get_title() {
return __( 'Plans', 'plugin-name' );
}
The get_title() method defines the display name of the widget in the Elementor editor. This is the name that users will see in the widget panel when they are building pages. Wrapping the title in the __() function makes it translatable, which is essential for multilingual websites. In this example, the title is set to “Plans.”
public function get_icon() {
return 'eicon-elementor';
}
The get_icon() method sets the icon that appears next to the widget name in the Elementor editor. This helps users quickly identify the widget visually. Elementor provides a set of default icons prefixed with eicon-, but you can also use custom icons if needed. In our widget, the default eicon-elementor is used.
public function get_categories() {
return [ 'webgarage' ];
}
The get_categories() method assigns the widget to one or more Elementor categories. Categories in Elementor help organize widgets in the editor, making them easier to find. In this example, the widget is placed in a custom category called webgarage, which we have already creared before.
protected function _register_controls() {
$this->start_controls_section(
'content_section',
[
'label' => __( 'Content', 'plugin-name' ),
'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
]
);
$this->add_control(
'plans',
[
'label' => __( 'Plans', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::REPEATER,
'fields' => [
[
'name' => 'popular',
'label' => __( 'Popular', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::SWITCHER,
'label_on' => __( 'Yes', 'plugin-name' ),
'label_off' => __( 'No', 'plugin-name' ),
'return_value' => 'yes',
'default' => '',
],
[
'name' => 'title',
'label' => __( 'Title', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => __( 'Service Title', 'plugin-name' ),
'label_block' => true,
],
[
'name' => 'description',
'label' => __( 'Description', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::TEXTAREA,
],
[
'name' => 'price',
'label' => __( 'Price', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => '',
],
[
'name' => 'period',
'label' => __( 'Period', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => '',
],
[
'name' => 'info',
'label' => __( 'Info', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::REPEATER,
'fields' => [
[
'name' => 'item',
'label' => __( 'Item', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => '',
'label_block' => true,
],
],
'title_field' => '{{{ item }}}',
],
[
'name' => 'button_label',
'label' => __( 'Button Label', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => __( 'Learn More', 'plugin-name' ),
'label_block' => true,
],
[
'name' => 'button_link',
'label' => __( 'Button Link', 'plugin-name' ),
'type' => \Elementor\Controls_Manager::URL,
'placeholder' => __( 'https://example.com', 'plugin-name' ),
],
],
'title_field' => '{{{ title }}}',
]
);
$this->end_controls_section();
}
The _register_controls() method helps you define all the editable settings of your widget that appear in the Elementor editor. It begins with $this->start_controls_section(), which starts a new section in the widget panel. Sections help organize controls into logical groups, labeled and optionally assigned to a tab (such as Content, Style, or Advanced). After starting a section, you add individual controls using $this->add_control() or $this->add_group_control(). Each control represents a field that the user can edit, such as text fields, textareas, URLs, switchers, or repeaters. In our Plans widget, we have a repeater control. It allows adding multiple plan items, each containing fields for title, description, price, period, features list, and button settings. Once all controls are added, $this->end_controls_section() closes the section.
Elementor provides a wide variety of control types that you can use, including: text, textarea, number, URL, switcher, select, etc. You can see the full list of available control types in the Elementor Developer Documentation. Using these controls, you can make your widget fully customizable and intuitive for users.
The render() method is responsible for generating the front-end output of the widget. First, we retrieve all the user-defined settings with $settings = $this->get_settings_for_display(). This gives us access to all the values entered in the widget’s fields, including titles, descriptions, prices, features, and button links. Using these settings, we then write the HTML structure of the widget, inserting dynamic content where needed. For example, we loop through each plan in a repeater field and display its title, description, price, period, feature list, and button. Conditional checks are often used to render elements only if the corresponding setting is not empty, such as showing a “Popular” badge only for plans marked as popular. This approach ensures that the widget output on the page matches exactly what the user configured in the Elementor editor.
Creating a custom Elementor widget may seem complex at first, but by breaking it down into steps—registering the widget, defining controls, and rendering the output—you can build fully customized elements tailored to your website’s needs. Using the example of the Plans widget, you’ve seen how to structure a widget class, add editable fields, handle repeaters, and output dynamic content on the front-end. Once you understand these principles, you can create virtually any widget, giving your Elementor-built site unique functionality and design that goes beyond the default options.
But if you need help with creating custom widgets, please contact us, and we will be glad to help you.