Build Custom Elementor Widgets — The Right Way!

How to Build Custom Elementor Widgets If you haven’t already checked out the Elementor plugin — you should. I used a lot of plug-n-play site builders in my time and this one by far blows the others out of the water. But you’re here to learn how to build custom Elementor widgets and guess what? … Read article

How to Build Custom Elementor Widgets

If you haven’t already checked out the Elementor plugin — you should. I used a lot of plug-n-play site builders in my time and this one by far blows the others out of the water. But you’re here to learn how to build custom Elementor widgets and guess what? It’s a cinch!

In this article, we’ll build a simple custom Elementor widget that allows the user to edit text, images and styles both in the widget settings and inline! We’ll call this widget, Elementor Awesomesauce.

Step 1. Create a plugin… don’t integrate in a theme.

I can’t tell you how many times I’ve seen custom implementations done in the theme (which is possible), but not great for the component approach. Building in a plugin allows you to have separation of code so your existing PHP, CSS and JS doesn’t get mangled up in your theme’s code. Even better, you can conditionally load your frontend assets only in the places where your custom widget will appear giving you a boost in performance. So let’s start there, create a Elementor Awesomesauce plugin:

  1. In wp-content/plugins, create a directory called elementor-awesomesauce.
  2. In the awesomesauce directory, add the following files & directories:

    • elementor-awesomesauce.php – initializes the plugin & verifies needed requirements
    • plugin.php – registers your custom widgets and scripts
    • widgets/awesomesauce.php – our custom Awesomesauce Elementor widget
    • assets/js/awesomesauce.js – some JS for our custom widget
    • assets/css – directory for our plugin specific CSS files

elementor-awesomesauce.php

<?php
/**
 * Plugin Name: Elementor Awesomesauce
 * Description: A simple Elementor Awesomesauce plugin that holds some most excellent custom widgets.
 * Plugin URI:  https://benmarshall.me/build-custom-elementor-widgets/
 * Version:     1.0.0
 * Author:      Ben Marshall
 * Author URI:  https://benmarshall.me
 * Text Domain: elementor-awesomesauce
 */

if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly

/**
 * Main Elementor Awesomesauce Class
 *
 * The init class that runs the Elementor Awesomesauce plugin.
 * Intended To make sure that the plugin's minimum requirements are met.
 *
 * You should only modify the constants to match your plugin's needs.
 *
 * Any custom code should go inside Plugin Class in the plugin.php file.
 * @since 1.0.0
 */
final class Elementor_Awesomesauce {

	/**
	 * Plugin Version
	 *
	 * @since 1.0.0
	 * @var string The plugin version.
	 */
	const VERSION = '1.0.0';

	/**
	 * Minimum Elementor Version
	 *
	 * @since 1.0.0
	 * @var string Minimum Elementor version required to run the plugin.
	 */
	const MINIMUM_ELEMENTOR_VERSION = '2.0.0';

	/**
	 * Minimum PHP Version
	 *
	 * @since 1.0.0
	 * @var string Minimum PHP version required to run the plugin.
	 */
	const MINIMUM_PHP_VERSION = '7.0';

	/**
	 * Constructor
	 *
	 * @since 1.0.0
	 * @access public
	 */
	public function __construct() {

		// Load translation
		add_action( 'init', array( $this, 'i18n' ) );

		// Init Plugin
		add_action( 'plugins_loaded', array( $this, 'init' ) );
	}

	/**
	 * Load Textdomain
	 *
	 * Load plugin localization files.
	 * Fired by `init` action hook.
	 *
	 * @since 1.2.0
	 * @access public
	 */
	public function i18n() {
		load_plugin_textdomain( 'elementor-awesomesauce' );
	}

	/**
	 * Initialize the plugin
	 *
	 * Validates that Elementor is already loaded.
	 * Checks for basic plugin requirements, if one check fail don't continue,
	 * if all check have passed include the plugin class.
	 *
	 * Fired by `plugins_loaded` action hook.
	 *
	 * @since 1.2.0
	 * @access public
	 */
	public function init() {

		// Check if Elementor installed and activated
		if ( ! did_action( 'elementor/loaded' ) ) {
			add_action( 'admin_notices', array( $this, 'admin_notice_missing_main_plugin' ) );
			return;
		}

		// Check for required Elementor version
		if ( ! version_compare( ELEMENTOR_VERSION, self::MINIMUM_ELEMENTOR_VERSION, '>=' ) ) {
			add_action( 'admin_notices', array( $this, 'admin_notice_minimum_elementor_version' ) );
			return;
		}

		// Check for required PHP version
		if ( version_compare( PHP_VERSION, self::MINIMUM_PHP_VERSION, '<' ) ) {
			add_action( 'admin_notices', array( $this, 'admin_notice_minimum_php_version' ) );
			return;
		}

		// Once we get here, We have passed all validation checks so we can safely include our plugin
		require_once( 'plugin.php' );
	}

	/**
	 * Admin notice
	 *
	 * Warning when the site doesn't have Elementor installed or activated.
	 *
	 * @since 1.0.0
	 * @access public
	 */
	public function admin_notice_missing_main_plugin() {
		if ( isset( $_GET['activate'] ) ) {
			unset( $_GET['activate'] );
		}

		$message = sprintf(
			/* translators: 1: Plugin name 2: Elementor */
			esc_html__( '"%1$s" requires "%2$s" to be installed and activated.', 'elementor-awesomesauce' ),
			'<strong>' . esc_html__( 'Elementor Awesomesauce', 'elementor-awesomesauce' ) . '</strong>',
			'<strong>' . esc_html__( 'Elementor', 'elementor-awesomesauce' ) . '</strong>'
		);

		printf( '<div class="notice notice-warning is-dismissible"><p>%1$s</p></div>', $message );
	}

	/**
	 * Admin notice
	 *
	 * Warning when the site doesn't have a minimum required Elementor version.
	 *
	 * @since 1.0.0
	 * @access public
	 */
	public function admin_notice_minimum_elementor_version() {
		if ( isset( $_GET['activate'] ) ) {
			unset( $_GET['activate'] );
		}

		$message = sprintf(
			/* translators: 1: Plugin name 2: Elementor 3: Required Elementor version */
			esc_html__( '"%1$s" requires "%2$s" version %3$s or greater.', 'elementor-awesomesauce' ),
			'<strong>' . esc_html__( 'Elementor Awesomesauce', 'elementor-awesomesauce' ) . '</strong>',
			'<strong>' . esc_html__( 'Elementor', 'elementor-awesomesauce' ) . '</strong>',
			self::MINIMUM_ELEMENTOR_VERSION
		);

		printf( '<div class="notice notice-warning is-dismissible"><p>%1$s</p></div>', $message );
	}

	/**
	 * Admin notice
	 *
	 * Warning when the site doesn't have a minimum required PHP version.
	 *
	 * @since 1.0.0
	 * @access public
	 */
	public function admin_notice_minimum_php_version() {
		if ( isset( $_GET['activate'] ) ) {
			unset( $_GET['activate'] );
		}

		$message = sprintf(
			/* translators: 1: Plugin name 2: PHP 3: Required PHP version */
			esc_html__( '"%1$s" requires "%2$s" version %3$s or greater.', 'elementor-awesomesauce' ),
			'<strong>' . esc_html__( 'Elementor Awesomesauce', 'elementor-awesomesauce' ) . '</strong>',
			'<strong>' . esc_html__( 'PHP', 'elementor-awesomesauce' ) . '</strong>',
			self::MINIMUM_PHP_VERSION
		);

		printf( '<div class="notice notice-warning is-dismissible"><p>%1$s</p></div>', $message );
	}
}

// Instantiate Elementor_Awesomesauce.
new Elementor_Awesomesauce();

plugin.php

<?php
namespace ElementorAwesomesauce;

/**
 * Class Plugin
 *
 * Main Plugin class
 * @since 1.0.0
 */
class Plugin {

	/**
	 * Instance
	 *
	 * @since 1.0.0
	 * @access private
	 * @static
	 *
	 * @var Plugin The single instance of the class.
	 */
	private static $_instance = null;

	/**
	 * Instance
	 *
	 * Ensures only one instance of the class is loaded or can be loaded.
	 *
	 * @since 1.2.0
	 * @access public
	 *
	 * @return Plugin An instance of the class.
	 */
	public static function instance() {
    if ( is_null( self::$_instance ) ) {
      self::$_instance = new self();
	  }
          
    return self::$_instance;
	}

	/**
	 * widget_scripts
	 *
	 * Load required plugin core files.
	 *
	 * @since 1.2.0
	 * @access public
	 */
	public function widget_scripts() {
		wp_register_script( 'elementor-awesomesauce', plugins_url( '/assets/js/awesomesauce.js', __FILE__ ), [ 'jquery' ], false, true );
	}

	/**
	 * Include Widgets files
	 *
	 * Load widgets files
	 *
	 * @since 1.2.0
	 * @access private
	 */
	private function include_widgets_files() {
		require_once( __DIR__ . '/widgets/awesomesauce.php' );
	}

	/**
	 * Register Widgets
	 *
	 * Register new Elementor widgets.
	 *
	 * @since 1.2.0
	 * @access public
	 */
	public function register_widgets() {
		// Its is now safe to include Widgets files
		$this->include_widgets_files();

		// Register Widgets
		\Elementor\Plugin::instance()->widgets_manager->register_widget_type( new Widgets\Awesomesauce() );
	}

	/**
	 *  Plugin class constructor
	 *
	 * Register plugin action hooks and filters
	 *
	 * @since 1.2.0
	 * @access public
	 */
	public function __construct() {

		// Register widget scripts
		add_action( 'elementor/frontend/after_register_scripts', [ $this, 'widget_scripts' ] );

		// Register widgets
		add_action( 'elementor/widgets/widgets_registered', [ $this, 'register_widgets' ] );
	}
}

// Instantiate Plugin Class
Plugin::instance();

widgets/awesomesauce.php

<?php
namespace ElementorAwesomesauce\Widgets;

use Elementor\Widget_Base;
use Elementor\Controls_Manager;

if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly

/**
 * @since 1.1.0
 */
class Awesomesauce extends Widget_Base {

	/**
	 * Retrieve the widget name.
	 *
	 * @since 1.1.0
	 *
	 * @access public
	 *
	 * @return string Widget name.
	 */
	public function get_name() {
		return 'awesomesauce';
	}

	/**
	 * Retrieve the widget title.
	 *
	 * @since 1.1.0
	 *
	 * @access public
	 *
	 * @return string Widget title.
	 */
	public function get_title() {
		return __( 'Awesomesauce', 'elementor-awesomesauce' );
	}

	/**
	 * Retrieve the widget icon.
	 *
	 * @since 1.1.0
	 *
	 * @access public
	 *
	 * @return string Widget icon.
	 */
	public function get_icon() {
		return 'fa fa-pencil';
	}

	/**
	 * Retrieve the list of categories the widget belongs to.
	 *
	 * Used to determine where to display the widget in the editor.
	 *
	 * Note that currently Elementor supports only one category.
	 * When multiple categories passed, Elementor uses the first one.
	 *
	 * @since 1.1.0
	 *
	 * @access public
	 *
	 * @return array Widget categories.
	 */
	public function get_categories() {
		return [ 'general' ];
	}

	/**
	 * Register the widget controls.
	 *
	 * Adds different input fields to allow the user to change and customize the widget settings.
	 *
	 * @since 1.1.0
	 *
	 * @access protected
	 */
	protected function _register_controls() {
		$this->start_controls_section(
			'section_content',
			[
				'label' => __( 'Content', 'elementor-awesomesauce' ),
			]
		);

		$this->add_control(
			'title',
			[
				'label' => __( 'Title', 'elementor-awesomesauce' ),
				'type' => Controls_Manager::TEXT,
				'default' => __( 'Title', 'elementor-awesomesauce' ),
			]
		);

		$this->add_control(
			'description',
			[
				'label' => __( 'Description', 'elementor-awesomesauce' ),
				'type' => Controls_Manager::TEXTAREA,
				'default' => __( 'Description', 'elementor-awesomesauce' ),
			]
		);

		$this->add_control(
			'content',
			[
				'label' => __( 'Content', 'elementor-awesomesauce' ),
				'type' => Controls_Manager::WYSIWYG,
				'default' => __( 'Content', 'elementor-awesomesauce' ),
			]
		);


		$this->end_controls_section();
	}

	/**
	 * Render the widget output on the frontend.
	 *
	 * Written in PHP and used to generate the final HTML.
	 *
	 * @since 1.1.0
	 *
	 * @access protected
	 */
	protected function render() {
		$settings = $this->get_settings_for_display();

		$this->add_inline_editing_attributes( 'title', 'none' );
		$this->add_inline_editing_attributes( 'description', 'basic' );
		$this->add_inline_editing_attributes( 'content', 'advanced' );
		?>
		<h2 <?php echo $this->get_render_attribute_string( 'title' ); ?>><?php echo $settings['title']; ?></h2>
		<div <?php echo $this->get_render_attribute_string( 'description' ); ?>><?php echo $settings['description']; ?></div>
		<div <?php echo $this->get_render_attribute_string( 'content' ); ?>><?php echo $settings['content']; ?></div>
		<?php
	}

	/**
	 * Render the widget output in the editor.
	 *
	 * Written as a Backbone JavaScript template and used to generate the live preview.
	 *
	 * @since 1.1.0
	 *
	 * @access protected
	 */
	protected function _content_template() {
		?>
		<#
		view.addInlineEditingAttributes( 'title', 'none' );
		view.addInlineEditingAttributes( 'description', 'basic' );
		view.addInlineEditingAttributes( 'content', 'advanced' );
		#>
		<h2 {{{ view.getRenderAttributeString( 'title' ) }}}>{{{ settings.title }}}</h2>
		<div {{{ view.getRenderAttributeString( 'description' ) }}}>{{{ settings.description }}}</div>
		<div {{{ view.getRenderAttributeString( 'content' ) }}}>{{{ settings.content }}}</div>
		<?php
	}
}

assets/js/awesomesauce.js

( function( $ ) {
	/**
 	 * @param $scope The Widget wrapper element as a jQuery element
	 * @param $ The jQuery alias
	 */ 
	var WidgetAwesomesauceHandler = function( $scope, $ ) {
		console.log( $scope );
	};
	
	// Make sure you run this code under Elementor.
	$( window ).on( 'elementor/frontend/init', function() {
		elementorFrontend.hooks.addAction( 'frontend/element_ready/awesomesauce.default', WidgetAwesomesauceHandler );
	} );
} )( jQuery );

Step 2. Add your custom fields.

Elementor is super powerful allowing you to easily add custom tabs, fields, style setting and even responsiveness. In the code above, the _register_controls is where we’ve already added a few fields for title, description and content, but what if you need more? Check out the examples below to add even more awesomeness to your Awesomesauce plugin.

Keep going with Elementor custom fields →

1 2

Comments

  1. Mark L Wood-Patrick February 25, 2020

    Many thanks for the presentation, very helpful. I have a question about php code style. When I look at the page: https://developers.elementor.com/getting-started/ it understandably recommends using phpcs using the wordpress coding standard but when I run phpcs on the elementor source a few problems are consistently reported:

    Class file names should be based on the class name with “class-” prepended. Expected class-control-button.php, but found button.php.

    Missing file doc comment

    Short array syntax is not allowed

    I’m using the current WordPress rules as far as I can see. Do you know where I can I find a phpcs rule set which matches what the elementor team are doing in practice? When I look at a well used plugin that works with Elementor such as Ocean Extra I see a lot more violations. Any pointers would be much appreciated.

    • That’s pretty common with WP plugins & themes. Unfortunately, it’s the wild west when it comes to code quality in WP plugins.

    • Mark L Wood-Patrick February 28, 2020

      Is there a github repository with the code from this article. I’m seeing some phpcs issues in my build of this code and would appreciate your comments on the reported issues , which issues I should ignore and which should be fixed and best way to fix them

      FOUND 10 ERRORS AND 2 WARNINGS AFFECTING 8 LINES
      ———————————————————————————————————————– 1 | ERROR | Class file names should be based on the class name with “class-” prepended. Expected
      | | class-awesomesauce.php, but found awesomesauce.php.
      1 | ERROR | Missing file doc comment
      12 | ERROR | Inline comments must end in full-stops, exclamation marks, or question marks
      15 | ERROR | Missing short description in doc comment
      87 | WARNING | Method name “_register_controls” should not be prefixed with an underscore to indicate visibility 217 | ERROR | All output should be run through an escaping function (see the Security sections in the WordPress | | Developer Handbooks), found ‘$this’.
      217 | ERROR | All output should be run through an escaping function (see the Security sections in the WordPress | | Developer Handbooks), found ‘$settings[‘title’]’.
      218 | ERROR | All output should be run through an escaping function (see the Security sections in the WordPress | | Developer Handbooks), found ‘$this’.
      218 | ERROR | All output should be run through an escaping function (see the Security sections in the WordPress | | Developer Handbooks), found ‘$settings[‘description’]’.
      219 | ERROR | All output should be run through an escaping function (see the Security sections in the WordPress | | Developer Handbooks), found ‘$this’.
      219 | ERROR | All output should be run through an escaping function (see the Security sections in the WordPress | | Developer Handbooks), found ‘$settings[‘content’]’.
      232 | WARNING | Method name “_content_template” should not be prefixed with an underscore to indicate visibility

  2. Built my first widget! Thanks for the help.

    I have a question. I found the Elementor documentation on adding a new category, but can’t figure out in which file and where to place the code. Does anyone know?

    • Put it in the Plugin class:

      public function register_categories( $elements_manager ) {
          $elements_manager->add_category(
            'categorykey',
            [
              'title' => __( 'Category Name', 'textdomain' ),
              'icon' => 'fa fa-plug',
            ]
          );
        }
  3. Awsome stuff.
    I was about to start on a basic plugin, then looked at the oceanwp elementor addons plugin which looked really complicated.
    Then I found this which is obviously what they used as well and now have a well-structured custom widgets plugin.

    Thank you!

  4. hi thanks for everything it was so much helpful .. can you please help me i want to know how can i register style just like script

    • Hello,
      Good tutorial,
      How can i add more widgets? Is there a way to make one file php for widget? Like elementor default have.

      Or what i want to do is to add some extra options to actual elementor widgets, ex background color and hover etc.

      Cheers

    • Craig Tommola February 13, 2020

      Same here – I followed this to the T and searched the docs too … adding to the plugins.php file didn’t work as expected …

      public function widget_styles() {
      wp_register_style( ‘elementor-awesomesauce’, plugins_url( ‘/assets/css/awesomesauce.css’, __FILE__ ) );
      }

      add_action( ‘elementor/frontend/after_enqueue_styles’, [ $this, ‘widget_styles’ ] );

    • In plugin.php, add this to the Plugin class:

      /**
         * widget_scripts
         *
         * Load required plugin core files.
         *
         * @access public
         */
        public function widget_scripts() {
          $plugin = get_plugin_data( __FILE__, false, false );
      
          wp_register_style( 'your-style', plugin_dir_url( __FILE__  ) . 'assets/css/file.css', [], $plugin['Version'] );
          wp_register_script( 'your-script', plugin_dir_url( __FILE__ ) . 'assets/js/file.js', [ 'jquery' ], $plugin['Version'], true );
        }
      

      Then in your widget class add the following to call the scripts when the widget is loaded:

      public function get_style_depends() {
          $styles = [ 'your-style' ];
      
          return $styles;
        }
      
        public function get_script_depends() {
          $scripts = [ 'your-script' ];
      
          return $scripts;
        }
      
  5. Sanjaya Dulal September 30, 2019

    Not working. I tired all above code .

    Well how can i implement it. Any idea ?

    i am bit confusion it.

  6. Himanshu July 17, 2019

    Wow , it worked perfectly ! Can you come up with any idea to register Custom Widget for “Single page” editor only ? I want to register a widget which will appear only in “Single” Editor .. Is it possible ?

    • If you’re trying to conditionally show/hide a widget in the frontend, try hooking into ‘elementor/widget/render_content’ .
      See https://code.elementor.com/php-hooks/#elementorwidgetrender_content

      If you’re trying to restrict certain widgets in the editor, I don’t think (maybe) there’s an action for that. Elementor provides a ‘Role Manager’ though. In the Pro version, there’s an option to ‘only allow them to edit existing content inside the editor’. This means you can publish/create a template beforehand, then whoever edits that page/template, can only edit the existing content in the editor (only the widgets you used for that template).

  7. Abe Caymo May 15, 2019

    Hey thanks for this great article btw!

    Could you add an example on responsive controls?
    More specifically, in the ‘Responsive Choose’ found in the https://developers.elementor.com/add-responsive-controls-to-widgets/ , what is the “prefix_class” used for?

    • That’s used to create a prefix for the alignment class. In their example, depending on the choice, the final class could be: content-align-left, content-align-right or content-align-center

  8. Abe Caymo May 10, 2019

    Just wow. I’m relatively new to coding and after going through this tutorial, now I understand what ‘good code documents itself’ means. Now I ACTUALLY understand more about OOP seeing it in action. Thanks a lot!

  9. This is really great! One thing I’m trying to do is create a custom Elementor widget that just runs some PHP where it is placed. Do you know if this is possible? I’ve been tinkering around but can’t quite get it to work. I actually don’t need any controls for the widget– it’s so I can place a custom action hook within my Elementor pages.

    • I’m sure there’s a plugin that enables PHP in the WP text widget. It might be possible to do it that way?

    • May not be understanding, but likely you don’t need the elementor widget. You just need to add your code into a core WP hook like init or wp. Which hook you choose will depend on your needs. You could also potentially create a simple shortcode that you can add into Elementor pages using the shortcode widget which can run anything you’d like with no output.

  10. Michael Bourne April 26, 2019

    This is better than the official docs. Kudos.

  11. Giovani Camara March 19, 2019

    This was ridiculously useful… I managed to get this all working which is great! Do you know if this awesomesauce plugin can only have one extension, or is it possible to have multiple widgets in this individual plugin? I renamed it all appropriate to my project, but I am having difficulties adding more widgets to it…. any ideas?

    • Glad you found it useful any Yup, you can add as many as you like! Look in plugin.php in the register_widgets method. Just follow the example for a new widget, then register it. Ex:

      private function include_widgets_files() {
        // First widget
        require_once( __DIR__ . '/widgets/awesomesauce.php' );
        // Second widget
        require_once( __DIR__ . '/widgets/awesomesauce2.php' );
      }
      
      public function register_widgets() {
        // Its is now safe to include Widgets files
        $this->include_widgets_files();
       
        // Register Widgets
        // First widget
        \Elementor\Plugin::instance()->widgets_manager->register_widget_type( new Widgets\Awesomesauce() );
        // Second widget
        \Elementor\Plugin::instance()->widgets_manager->register_widget_type( new Widgets\Awesomesauce2() );
      }

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

All comments are held for moderation and only published when on topic and not rude. You'll even get little stars if you do an extra good job.

You may write comments in Markdown. This is the best way to post any code, inline like `<div>this</div>` or multiline blocks within triple backtick fences (```) with double new lines before and after.

Want to tell me something privately, like pointing out a typo or stuff like that? Contact Me.

icon-search icon-link