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. 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).

  2. 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?

  3. 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!

  4. 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?

  5. Michael Bourne April 26, 2019

    This is better than the official docs. Kudos.

  6. 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