
🛠️ Кастомні віджети 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-полями та оптимізацію виведення.



