Skip to content

Всё для WordPress, веб-разработки — и не только

🛠️ Кастомные виджеты Elementor: полный цикл от плагина до контролов

🛠️ Кастомные виджеты 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. Минимальная структура файлов:

1elementor-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
11if ( ! defined( 'ABSPATH' ) ) exit;
12
13final 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
86new 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
2namespace ElementorAwesomesauce;
3
4use Elementor\Plugin as ElementorPlugin;
5
6class 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
42Plugin::instance();

Обратите внимание: register_widgets принимает параметр $widgets_manager. В новом хуке менеджер передаётся напрямую, а не достаётся через ElementorPlugin::instance()->widgets_manager. Чище и без лишнего импорта.

Класс виджета: наследование, контролы и рендер

Файл widgets/awesomesauce.php, сердце плагина. Наследуем Widget_Base, определяем имя, заголовок, иконку и категорию. Метод register_controls() добавляет три текстовых поля: однострочное, многострочное и WYSIWYG. Метод render() выводит разметку на фронтенде, а content_template() задаёт Backbone-шаблон для live-превью в редакторе.

1<?php
2namespace ElementorAwesomesauceWidgets;
3
4use Elementor\Widget_Base;
5use Elementor\Controls_Manager;
6
7if ( ! defined( 'ABSPATH' ) ) exit;
8
9class 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 вообще.

1use 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_registeredelementor/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-полями и оптимизация вывода.