
🛠️ Кастомные виджеты Elementor: полный цикл от плагина до контролов
Обновили тему, и кастомный блок, который показывал акции на главной, разъехался вёрсткой. Знакомая ситуация? Код лежит в functions.php, стили размазаны по style.css, а JS-обработчик прибит гвоздями к footer.php. Смена темы или крупное обновление Elementor, и всё это приходится собирать заново.
Проблема не в том, что «код плохой». Проблема в том, что он живёт не там. Кастомные доработки, которые пихают в тему, привязаны к ней намертво: перенести на другой сайт невозможно, отлаживать конфликты мучительно, а условная загрузка ассетов отсутствует как класс.
Правильный путь, оформить кастомный виджет в отдельный плагин. Ровно так официальная документация Elementor рекомендует поступать с любыми нетривиальными доработками. Изоляция кода, независимость от темы, нормальная версионность и загрузка JS только на страницах с виджетом. В этом руководстве, полный цикл: от структуры плагина до живого виджета с inline-редактированием, на актуальном API Elementor (3.x/4.x).

💡 Быстрый обзор:
- Регистрируем отдельный плагин для виджета, изолируем код от темы раз и навсегда.
- Собираем главный класс с проверками совместимости: Elementor активен, версия не ниже 3.5, PHP от 7.4.
- Создаём класс виджета: наследуем
Widget_Base, определяемregister_controls()и метод рендера. - Подключаем inline-редактирование текста через
add_inline_editing_attributes()и Backbone-шаблон. - Добавляем кастомные поля: выбор картинки из медиатеки, выпадающие списки, группу кнопок, типографику.
- Цепляем JavaScript-обработчик к хуку
elementor/frontend/init, точка входа для любой клиентской логики.
Структура плагина и главный класс
Создадим плагин Elementor Awesomesauce. Минимальная структура файлов:
1 elementor-awesomesauce/ 2 ├── elementor-awesomesauce.php ← точка входа 3 ├── plugin.php ← класс-синглтон плагина 4 ├── widgets/ 5 │ └── awesomesauce.php ← класс виджета 6 └── assets/ 7 └── js/ 8 └── awesomesauce.js ← JS-обработчик фронтенда
Главный файл плагина, стандартный WordPress-заголовок плюс класс-загрузчик с проверками совместимости. Именно такую структуру описывает руководство Elementor по первому аддону. Код ниже работает в Elementor 3.5+ и Elementor 4.x: методы без устаревшего префикса _, константы под современные версии PHP.
Добавьте этот код в elementor-awesomesauce.php. Файл должен лежать в /wp-content/plugins/elementor-awesomesauce/. Перед активацией сделайте полный бэкап сайта.
1 <?php 2 /** 3 * Plugin Name: Elementor Awesomesauce 4 * Description: Кастомный виджет Elementor с живым редактированием текста, картинок и стилей. 5 * Plugin URI: https://techblog.sdstudio.top/ 6 * Version: 1.0.0 7 * Author: TechBlog SD Studio 8 * Text Domain: elementor-awesomesauce 9 */ 10 11 if ( ! defined( 'ABSPATH' ) ) exit; 12 13 final class Elementor_Awesomesauce { 14 15 const VERSION = '1.0.0'; 16 const MINIMUM_ELEMENTOR_VERSION = '3.5.0'; 17 const MINIMUM_PHP_VERSION = '7.4'; 18 19 public function __construct() { 20 add_action( 'init', array( $this, 'i18n' ) ); 21 add_action( 'plugins_loaded', array( $this, 'init' ) ); 22 } 23 24 public function i18n() { 25 load_plugin_textdomain( 'elementor-awesomesauce' ); 26 } 27 28 public function init() { 29 if ( ! did_action( 'elementor/loaded' ) ) { 30 add_action( 'admin_notices', array( $this, 'admin_notice_missing_main_plugin' ) ); 31 return; 32 } 33 34 if ( ! version_compare( ELEMENTOR_VERSION, self::MINIMUM_ELEMENTOR_VERSION, '>=' ) ) { 35 add_action( 'admin_notices', array( $this, 'admin_notice_minimum_elementor_version' ) ); 36 return; 37 } 38 39 if ( version_compare( PHP_VERSION, self::MINIMUM_PHP_VERSION, '<' ) ) { 40 add_action( 'admin_notices', array( $this, 'admin_notice_minimum_php_version' ) ); 41 return; 42 } 43 44 require_once( 'plugin.php' ); 45 } 46 47 public function admin_notice_missing_main_plugin() { 48 if ( isset( $_GET['activate'] ) ) { 49 unset( $_GET['activate'] ); 50 } 51 $message = sprintf( 52 esc_html__( '"%1$s" requires "%2$s" to be installed and activated.', 'elementor-awesomesauce' ), 53 '<strong>' . esc_html__( 'Elementor Awesomesauce', 'elementor-awesomesauce' ) . '</strong>', 54 '<strong>' . esc_html__( 'Elementor', 'elementor-awesomesauce' ) . '</strong>' 55 ); 56 printf( '<p>%1$s</p>', $message ); 57 } 58 59 public function admin_notice_minimum_elementor_version() { 60 if ( isset( $_GET['activate'] ) ) { 61 unset( $_GET['activate'] ); 62 } 63 $message = sprintf( 64 esc_html__( '"%1$s" requires "%2$s" version %3$s or greater.', 'elementor-awesomesauce' ), 65 '<strong>' . esc_html__( 'Elementor Awesomesauce', 'elementor-awesomesauce' ) . '</strong>', 66 '<strong>' . esc_html__( 'Elementor', 'elementor-awesomesauce' ) . '</strong>', 67 self::MINIMUM_ELEMENTOR_VERSION 68 ); 69 printf( '<p>%1$s</p>', $message ); 70 } 71 72 public function admin_notice_minimum_php_version() { 73 if ( isset( $_GET['activate'] ) ) { 74 unset( $_GET['activate'] ); 75 } 76 $message = sprintf( 77 esc_html__( '"%1$s" requires "%2$s" version %3$s or greater.', 'elementor-awesomesauce' ), 78 '<strong>' . esc_html__( 'Elementor Awesomesauce', 'elementor-awesomesauce' ) . '</strong>', 79 '<strong>' . esc_html__( 'PHP', 'elementor-awesomesauce' ) . '</strong>', 80 self::MINIMUM_PHP_VERSION 81 ); 82 printf( '<p>%1$s</p>', $message ); 83 } 84 } 85 86 new Elementor_Awesomesauce();
Что здесь важно. Константа MINIMUM_ELEMENTOR_VERSION, 3.5.0. Именно с этой версии вступили в силу новые соглашения об именовании хуков и методов без префикса _. Для Elementor 4.x (актуальная версия на июнь 2026) этот же код работает без изменений, виджетное API в 4.x не ломали. PHP, минимум 7.4, но на практике 8.x уже стандарт де-факто. Класс объявлен final: это точка входа, наследоваться от него не нужно.
Класс Plugin: синглтон и регистрация виджета
Файл plugin.php, синглтон, который подключает JS-ассеты и регистрирует виджет через хук elementor/widgets/register. Это главное изменение по сравнению со старыми руководствами: хук elementor/widgets/widgets_registered объявлен устаревшим с версии 3.5.0; метод register_widget_type() заменён на register().
1 <?php 2 namespace ElementorAwesomesauce; 3 4 use Elementor\Plugin as ElementorPlugin; 5 6 class Plugin { 7 8 private static $_instance = null; 9 10 public static function instance() { 11 if ( is_null( self::$_instance ) ) { 12 self::$_instance = new self(); 13 } 14 return self::$_instance; 15 } 16 17 public function widget_scripts() { 18 wp_register_script( 19 'elementor-awesomesauce', 20 plugins_url( '/assets/js/awesomesauce.js', __FILE__ ), 21 [ 'jquery' ], 22 false, 23 true 24 ); 25 } 26 27 private function include_widgets_files() { 28 require_once( __DIR__ . '/widgets/awesomesauce.php' ); 29 } 30 31 public function register_widgets( $widgets_manager ) { 32 $this->include_widgets_files(); 33 $widgets_manager->register( new \ElementorAwesomesauceWidgets\Awesomesauce() ); 34 } 35 36 public function __construct() { 37 add_action( 'elementor/frontend/after_register_scripts', [ $this, 'widget_scripts' ] ); 38 add_action( 'elementor/widgets/register', [ $this, 'register_widgets' ] ); 39 } 40 } 41 42 Plugin::instance();
Обратите внимание: register_widgets принимает параметр $widgets_manager. В новом хуке менеджер передаётся напрямую, а не достаётся через ElementorPlugin::instance()->widgets_manager. Чище и без лишнего импорта.
Класс виджета: наследование, контролы и рендер
Файл widgets/awesomesauce.php, сердце плагина. Наследуем Widget_Base, определяем имя, заголовок, иконку и категорию. Метод register_controls() добавляет три текстовых поля: однострочное, многострочное и WYSIWYG. Метод render() выводит разметку на фронтенде, а content_template() задаёт Backbone-шаблон для live-превью в редакторе.
1 <?php 2 namespace ElementorAwesomesauceWidgets; 3 4 use Elementor\Widget_Base; 5 use Elementor\Controls_Manager; 6 7 if ( ! defined( 'ABSPATH' ) ) exit; 8 9 class Awesomesauce extends Widget_Base { 10 11 public function get_name() { 12 return 'awesomesauce'; 13 } 14 15 public function get_title() { 16 return __( 'Awesomesauce', 'elementor-awesomesauce' ); 17 } 18 19 public function get_icon() { 20 return 'eicon-pencil'; 21 } 22 23 public function get_categories() { 24 return [ 'general' ]; 25 } 26 27 public function get_keywords() { 28 return [ 'awesomesauce', 'custom', 'demo' ]; 29 } 30 31 protected function register_controls() { 32 $this->start_controls_section( 33 'section_content', 34 [ 35 'label' => __( 'Content', 'elementor-awesomesauce' ), 36 ] 37 ); 38 39 $this->add_control( 40 'title', 41 [ 42 'label' => __( 'Title', 'elementor-awesomesauce' ), 43 'type' => Controls_Manager::TEXT, 44 'default' => __( 'Заголовок', 'elementor-awesomesauce' ), 45 ] 46 ); 47 48 $this->add_control( 49 'description', 50 [ 51 'label' => __( 'Description', 'elementor-awesomesauce' ), 52 'type' => Controls_Manager::TEXTAREA, 53 'default' => __( 'Описание', 'elementor-awesomesauce' ), 54 ] 55 ); 56 57 $this->add_control( 58 'content', 59 [ 60 'label' => __( 'Content', 'elementor-awesomesauce' ), 61 'type' => Controls_Manager::WYSIWYG, 62 'default' => __( 'Контент', 'elementor-awesomesauce' ), 63 ] 64 ); 65 66 $this->end_controls_section(); 67 } 68 69 protected function render() { 70 $settings = $this->get_settings_for_display(); 71 72 $this->add_inline_editing_attributes( 'title', 'none' ); 73 $this->add_inline_editing_attributes( 'description', 'basic' ); 74 $this->add_inline_editing_attributes( 'content', 'advanced' ); 75 ?> 76 <div class="elementor-awesomesauce"> 77 <h2 <?php $this->print_render_attribute_string( 'title' ); ?>> 78 <?php $this->print_unescaped_setting( 'title' ); ?> 79 </h2> 80 <div <?php $this->print_render_attribute_string( 'description' ); ?>> 81 <?php $this->print_unescaped_setting( 'description' ); ?> 82 </div> 83 <div <?php $this->print_render_attribute_string( 'content' ); ?>> 84 <?php $this->print_unescaped_setting( 'content' ); ?> 85 </div> 86 </div> 87 <?php 88 } 89 90 protected function content_template() { 91 ?> 92 <# 93 view.addInlineEditingAttributes( 'title', 'none' ); 94 view.addInlineEditingAttributes( 'description', 'basic' ); 95 view.addInlineEditingAttributes( 'content', 'advanced' ); 96 #> 97 <div class="elementor-awesomesauce"> 98 <h2 {{{ view.getRenderAttributeString( 'title' ) }}}>{{{ settings.title }}}</h2> 99 <div {{{ view.getRenderAttributeString( 'description' ) }}}>{{{ settings.description }}}</div> 100 <div {{{ view.getRenderAttributeString( 'content' ) }}}>{{{ settings.content }}}</div> 101 </div> 102 <?php 103 } 104 }
Что изменилось относительно устаревших руководств. Методы register_controls() и content_template() пишутся без префикса _, это изменение с Elementor 3.1. Для вывода в render() используются print_render_attribute_string() и print_unescaped_setting() вместо прямого echo, современный подход, рекомендованный с Elementor 3.x и работающий в 4.x. Иконка заменена с fa fa-pencil (Font Awesome 4, удалён из ядра) на eicon-pencil из нативного набора Elementor.
Кастомные поля: медиа, селекты, типографика
Текстовыми полями возможности не ограничиваются. Разберём четыре типа контролов, которые закрывают большинство реальных сценариев.
Медиа-поле
Controls_Manager::MEDIA добавляет стандартный выбор картинки из медиатеки WordPress. Метод Utils::get_placeholder_image_src() работает в актуальных версиях и даёт серую заглушку, если изображение не выбрано.
1 $this->add_control( 2 'mask_image', 3 [ 4 'label' => __( 'Mask Image', 'elementor-awesomesauce' ), 5 'type' => Controls_Manager::MEDIA, 6 'default' => [ 7 'url' => \Elementor\Utils::get_placeholder_image_src(), 8 ], 9 ] 10 );
Выпадающий список
Controls_Manager::SELECT, выбор из предопределённых значений. Ниже пример для HTML-тега заголовка:
1 $this->add_control( 2 'title_tag', 3 [ 4 'label' => __( 'Title HTML Tag', 'elementor-awesomesauce' ), 5 'type' => Controls_Manager::SELECT, 6 'default' => 'h2', 7 'options' => [ 8 'h1' => 'H1', 9 'h2' => 'H2', 10 'h3' => 'H3', 11 'h4' => 'H4', 12 ], 13 ] 14 );
Группа кнопок
Controls_Manager::CHOOSE показывает ряд иконок для визуального выбора. Иконки, только из набора eicon-*, не fa fa-*:
1 $this->add_control( 2 'text_align', 3 [ 4 'label' => __( 'Alignment', 'elementor-awesomesauce' ), 5 'type' => Controls_Manager::CHOOSE, 6 'options' => [ 7 'left' => [ 8 'title' => __( 'Left', 'elementor-awesomesauce' ), 9 'icon' => 'eicon-text-align-left', 10 ], 11 'center' => [ 12 'title' => __( 'Center', 'elementor-awesomesauce' ), 13 'icon' => 'eicon-text-align-center', 14 ], 15 'right' => [ 16 'title' => __( 'Right', 'elementor-awesomesauce' ), 17 'icon' => 'eicon-text-align-right', 18 ], 19 ], 20 'default' => 'center', 21 'toggle' => true, 22 ] 23 );
Типографика через Group Control
Групповой контрол типографики даёт полный набор: шрифт, размер, межбуквенное расстояние, насыщенность, и всё с адаптивными брейкпойнтами. Подробнее в документации Group_Control_Typography. Ключевой момент: Scheme_Typography устарел с Elementor 3.x, не используйте его. Вместо этого либо привязывайтесь к global-стилям, либо опускайте ключ scheme вообще.
1 use Elementor\Group_Control_Typography; 2 3 $this->add_group_control( 4 Group_Control_Typography::get_type(), 5 [ 6 'name' => 'content_typography', 7 'label' => __( 'Typography', 'elementor-awesomesauce' ), 8 'selector' => '{{WRAPPER}} .elementor-awesomesauce', 9 'fields_options' => [ 10 'letter_spacing' => [ 11 'range' => [ 12 'min' => 0, 13 'max' => 100, 14 ], 15 ], 16 ], 17 ] 18 );
Почему плагин, а не functions.php
Кастомный виджет в теме работает. Но ровно до смены темы. В отдельном плагине вы получаете три важных преимущества:
- Условная загрузка. Elementor вызывает
widget_scriptsтолько когда виджет реально отображается на странице, не на всём сайте подряд. Для проектов с десятками виджетов это ощутимая экономия HTTP-запросов. - Изоляция. PHP-логика, CSS и JS не смешиваются с темой. Найти и поправить баг, минуты, а не часы раскопок в
functions.phpна 2000 строк. - Переносимость. Активировал плагин на другом сайте, виджет работает. Без копипасты и ручной правки путей.

Видео: живой показ от пустого плагина до работающего виджета
Теория, хорошо, но наблюдать за кодом в действии, быстрее. В этом 30-минутном руководстве автор проходит весь путь: от пустой папки до виджета с контролами и рендером.
⁉️🤔 Частые вопросы
Почему виджет не появляется в панели Elementor?
Первое, проверьте хук регистрации. С Elementor 3.5 используется
elementor/widgets/register(неelementor/widgets/widgets_registered, он устарел). Второе: методregister_controls()должен быть без префикса_. Третье:get_categories()обязан возвращать массив с существующей категорией,'general'работает всегда. Четвёртое: очистите кеш WordPress после активации плагина.
Чем print_render_attribute_string() отличается от прямого echo в render()?
print_render_attribute_string()автоматически применяет фильтры атрибутов, включая inline-редактирование и data-атрибуты Elementor. Прямойechoэтих данных не даст, inline-редактирование просто не включится. Для вывода значений настроек используйтеprint_unescaped_setting(), он корректно обрабатывает экранирование внутри контролов.
Можно ли обойтись без отдельного JS-файла?
Да, если виджет чисто рендерит PHP-разметку без интерактивности. Но как только появляются слайдеры, анимации, AJAX-подгрузка или любая динамика на клиенте, JS обязателен. Даже минимальный обработчик (как в примере выше) даёт точку входа для будущей логики, не требуя переписывать регистрацию потом.
Обязателен ли namespace в плагине?
Формально нет. Но без namespace вы рискуете получить конфликт имён классов с другим плагином или темой, имена вроде
PluginилиWidgetдалеко не уникальны. Для продакшена, обязательно. ПрефиксElementorAwesomesauceWidgetsпрактически гарантирует уникальность.
Как обновить старый виджет, написанный по устаревшим руководствам?
План миграции описан в официальном руководстве Elementor по депрекациям: (1) переименуйте методы с префиксом
_,_register_controls()→register_controls(),_content_template()→content_template(); (2) замените хук наelementor/widgets/register, а метод регистрации, на$manager->register(); (3) заменитеfa fa-*иконки наeicon-*, уберитеScheme_Typography. После правок поднимитеMINIMUM_ELEMENTOR_VERSIONдо актуальной и протестируйте виджет на трёх уровнях: панель редактора, live-превью, фронтенд.
Что делать со старыми виджетами: план миграции на актуальное API
Если у вас уже есть кастомные виджеты, написанные по руководствам 2019-2021 годов, не паникуйте. Elementor держит обратную совместимость с депрекейшн-хендлерами до 8 мажорных версий. Но лучше обновить код сейчас, пока депрекейшн-нотисы не превратились в фатальные ошибки.
Чек-лист миграции:
- **Методы без **
_: везде, где видите_register_controlsи_content_template, убирайте префикс. - Хук регистрации:
elementor/widgets/widgets_registered→elementor/widgets/register. - Метод регистрации:
$manager->register_widget_type()→$manager->register(). - Иконки:
fa fa-*→eicon-*(нативный набор Elementor) илиdashicons-*(набор WordPress). - Типографика:
Scheme_Typography→ либо global-стили через'global' => [...], либо прямые значения без ключаscheme.
После изменений протестируйте виджет на трёх уровнях: открывается ли панель с контролами в редакторе, работает ли live-превью (Backbone-шаблон), рендерится ли фронтенд без ошибок. И помните про dry-run перед выкладкой на прод: Elementor молча пропускает битые контролы, не роняя всю страницу, поэтому визуальная проверка обязательна.
Старый код в functions.php или плагин с нуля: что выбрать
Если вы начинаете новый проект, только плагин. Аргументов «за» functions.php не осталось: даже для микро-виджета из 20 строк структура плагина окупается первым же обновлением темы.
Если у вас уже есть рабочий код в теме, выделите его в плагин при ближайшем рефакторинге. Процесс прямолинейный: создайте папку и главный файл плагина по структуре выше, перенесите класс виджета в widgets/, пропишите регистрацию через elementor/widgets/register и протестируйте на стейдже. На практике это занимает 15-20 минут для типового виджета.
Начните с базового класса из этого руководства, скопируйте главный файл и plugin.php как каркас, замените название виджета на своё. А когда появятся вопросы, загляните в раздел Widgets на developers.elementor.com: там разобраны рендеринг медиа, работа с repeater-полями и оптимизация вывода.



