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