
🛠️ Niestandardowe widgety Elementor: pełny cykl od wtyczki do kontrolek
Zaktualizowali Państwo motyw i niestandardowy blok, który wyświetlał promocje na stronie głównej, rozjechał się wizualnie. Znajoma sytuacja? Kod leży w functions.php, style są rozsmarowane po style.css, a obsługa JS przybita gwoździami do footer.php. Zmiana motywu lub duża aktualizacja Elementora i wszystko to trzeba składać od nowa.
Problem nie w tym, że „kod jest zły". Problem w tym, że znajduje się nie tam, gdzie powinien. Niestandardowe modyfikacje upychane w motywie są z nim trwale związane: przeniesienie na inną stronę jest niemożliwe, debugowanie konfliktów bolesne, a warunkowe ładowanie zasobów nie istnieje.
Właściwą ścieżką jest wydzielenie niestandardowego widżetu do osobnej wtyczki. Dokładnie tak oficjalna dokumentacja Elementora zaleca postępować z wszelkimi nietrywialnymi modyfikacjami. Izolacja kodu, niezależność od motywu, normalne wersjonowanie i ładowanie JS tylko na stronach z widżetem. W tym poradniku: pełny cykl, od struktury wtyczki po działający widżet z edycją inline, na aktualnym API Elementora (3.x/4.x).

💡 Szybki przegląd:
- Rejestrujemy osobną wtyczkę dla widżetu, izolujemy kod od motywu raz na zawsze.
- Budujemy główną klasę z kontrolą zgodności: Elementor aktywny, wersja nie niższa niż 3.5, PHP od 7.4.
- Tworzymy klasę widżetu: dziedziczymy
Widget_Base, definiujemyregister_controls()i metodę renderowania. - Podłączamy edycję tekstu inline przez
add_inline_editing_attributes()i szablon Backbone. - Dodajemy niestandardowe pola: wybór obrazu z biblioteki mediów, listy rozwijane, grupę przycisków, typografię.
- Podpinamy obsługę JavaScript do haka
elementor/frontend/init, punktu wejścia dla dowolnej logiki po stronie klienta.
Struktura wtyczki i główna klasa
Stwórzmy wtyczkę Elementor Awesomesauce. Minimalna struktura plików:
1 elementor-awesomesauce/ 2 ├── elementor-awesomesauce.php ← точка входа 3 ├── plugin.php ← класс-синглтон плагина 4 ├── widgets/ 5 │ └── awesomesauce.php ← класс виджета 6 └── assets/ 7 └── js/ 8 └── awesomesauce.js ← JS-обработчик фронтенда
Główny plik wtyczki, standardowy nagłówek WordPress plus klasa ładująca z kontrolą zgodności. Właśnie taką strukturę opisuje poradnik Elementora dotyczący pierwszego addonu. Kod poniżej działa w Elementorze 3.5+ oraz Elementorze 4.x: metody bez przestarzałego prefiksu _, stałe dostosowane do współczesnych wersji PHP.
Proszę dodać ten kod do elementor-awesomesauce.php. Plik musi znajdować się w /wp-content/plugins/elementor-awesomesauce/. Przed aktywacją proszę wykonać pełną kopię zapasową strony.
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();
Co jest tu ważne. Stała MINIMUM_ELEMENTOR_VERSION, 3.5.0. Właśnie od tej wersji weszły w życie nowe konwencje nazewnictwa haków i metod bez prefiksu _. Dla Elementora 4.x (aktualna wersja na czerwiec 2026) ten sam kod działa bez zmian, API widżetów w 4.x nie zostało naruszone. PHP co najmniej 7.4, ale w praktyce 8.x jest już de facto standardem. Klasa zadeklarowana jako final: to punkt wejścia, nie ma potrzeby po niej dziedziczyć.
Klasa Plugin: singleton i rejestracja widżetu
Plik plugin.php, singleton, który podłącza zasoby JS i rejestruje widżet przez hak elementor/widgets/register. To główna zmiana w porównaniu ze starymi poradnikami: hak elementor/widgets/widgets_registered został uznany za przestarzały od wersji 3.5.0; metodę register_widget_type() zastąpiono metodą 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();
Proszę zwrócić uwagę: register_widgets przyjmuje parametr $widgets_manager. W nowym haku menedżer jest przekazywany bezpośrednio, a nie pobierany przez ElementorPlugin::instance()->widgets_manager. Czyściej i bez zbędnego importu.
Klasa widżetu: dziedziczenie, kontrolki i renderowanie
Plik widgets/awesomesauce.php, serce wtyczki. Dziedziczymy Widget_Base, definiujemy nazwę, tytuł, ikonę i kategorię. Metoda register_controls() dodaje trzy pola tekstowe: jednowierszowe, wielowierszowe i WYSIWYG. Metoda render() wyświetla znaczniki na frontendzie, a content_template() definiuje szablon Backbone dla podglądu na żywo w edytorze.
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 }
Co zmieniło się w porównaniu z przestarzałymi poradnikami. Metody register_controls() i content_template() zapisuje się bez prefiksu _, to zmiana z Elementor 3.1. Do wyprowadzania w render() używa się print_render_attribute_string() i print_unescaped_setting() zamiast bezpośredniego echo, nowoczesne podejście rekomendowane od Elementor 3.x, które działa w 4.x. Ikonę zmieniono z fa fa-pencil (Font Awesome 4, usunięty z rdzenia) na eicon-pencil z natywnego zestawu Elementor.
Pola niestandardowe: media, selektory, typografia
Możliwości nie ograniczają się do pól tekstowych. Omówimy cztery typy kontrolek, które pokrywają większość rzeczywistych scenariuszy.
Pole mediów
Controls_Manager::MEDIA dodaje standardowy wybór obrazu z biblioteki mediów WordPress. Metoda Utils::get_placeholder_image_src() działa w aktualnych wersjach i zwraca szary placeholder, jeśli obraz nie został wybrany.
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 );
Lista rozwijana
Controls_Manager::SELECT, wybór spośród wstępnie zdefiniowanych wartości. Poniżej przykład dla znacznika HTML nagłówka:
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 );
Grupa przycisków
Controls_Manager::CHOOSE pokazuje rząd ikon do wyboru wizualnego. Ikony, wyłącznie z zestawu eicon-*, nie 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 );
Typografia przez Group Control
Grupowa kontrolka typografii daje pełen zestaw: czcionkę, rozmiar, odstępy między literami, grubość, a wszystko z adaptacyjnymi punktami granicznymi. Więcej szczegółów w dokumentacji Group_Control_Typography. Kluczowa kwestia: Scheme_Typography jest przestarzały od Elementor 3.x, proszę go nie używać. Zamiast tego należy albo wiązać się ze stylami globalnymi, albo całkowicie pomijać klucz 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 );
Dlaczego wtyczka, a nie functions.php
Niestandardowy widget w motywie działa. Ale tylko do momentu zmiany motywu. W osobnej wtyczce otrzymują Państwo trzy istotne korzyści:
- Ładowanie warunkowe. Elementor wywołuje
widget_scriptstylko wtedy, gdy widget jest faktycznie wyświetlany na stronie, a nie na całej witrynie hurtowo. Dla projektów z dziesiątkami widgetów to odczuwalna oszczędność zapytań HTTP. - Izolacja. Logika PHP, CSS i JS nie mieszają się z motywem. Znalezienie i naprawienie błędu to minuty, a nie godziny przekopywania się przez
functions.phpliczący 2000 linii. - Przenośność. Aktywują Państwo wtyczkę na innej stronie i widget działa. Bez kopiowania i ręcznego poprawiania ścieżek.

Wideo: pokaz na żywo od pustej wtyczki do działającego widgetu
Teoria jest dobra, ale obserwowanie kodu w działaniu jest szybsze. W tym 30-minutowym poradniku autor przechodzi całą drogę: od pustego folderu do widgetu z kontrolkami i renderowaniem.
⁉️🤔 Często zadawane pytania
Dlaczego widget nie pojawia się w panelu Elementora?
Po pierwsze: proszę sprawdzić hook rejestracji. Od Elementora 3.5 używamy
elementor/widgets/register(nieelementor/widgets/widgets_registered, jest on przestarzały). Po drugie: metodaregister_controls()musi być bez prefiksu_. Po trzecie:get_categories()musi zwracać tablicę z istniejącą kategorią,'general'działa zawsze. Po czwarte: proszę wyczyścić cache WordPressa po aktywacji wtyczki.
Czym print_render_attribute_string() różni się od bezpośredniego echo w render()?
print_render_attribute_string()automatycznie stosuje filtry atrybutów, w tym edycję inline i data-atrybuty Elementora. Bezpośrednieechotych danych nie dostarczy, edycja inline po prostu się nie włączy. Do wyprowadzania wartości ustawień proszę używaćprint_unescaped_setting(), poprawnie obsługuje on eskejpowanie wewnątrz kontrolek.
Czy można obejść się bez osobnego pliku JS?
Tak, jeśli widget wyłącznie renderuje znaczniki PHP bez interaktywności. Ale gdy tylko pojawiają się slidery, animacje, ładowanie AJAX lub jakakolwiek dynamika po stronie klienta, JS jest obowiązkowy. Nawet minimalny handler (jak w przykładzie powyżej) daje punkt wejścia dla przyszłej logiki, nie wymagając późniejszego przepisywania rejestracji.
Czy namespace we wtyczce jest obowiązkowy?
Formalnie nie. Ale bez namespace ryzykują Państwo konflikt nazw klas z inną wtyczką lub motywem, nazwy w rodzaju
PluginczyWidgetnie są unikalne. Dla produkcji: obowiązkowo. PrefiksElementorAwesomesauceWidgetspraktycznie gwarantuje unikalność.
Jak zaktualizować stary widget napisany według przestarzałych poradników?
Plan migracji opisano w oficjalnym poradniku Elementora dotyczącym deprekacji: (1) proszę zmienić nazwy metod z prefiksem
_,_register_controls()→register_controls(),_content_template()→content_template(); (2) proszę zastąpić hook naelementor/widgets/register, a metodę rejestracji na$manager->register(); (3) proszę zastąpić ikonyfa fa-*naeicon-*, usunąćScheme_Typography. Po poprawkach proszę podnieśćMINIMUM_ELEMENTOR_VERSIONdo aktualnej i przetestować widget na trzech poziomach: panel edytora, podgląd live, frontend.
Co zrobić ze starymi widgetami: plan migracji na aktualne API
Jeśli mają już Państwo własne widgety napisane według poradników z lat 2019-2021, proszę nie panikować. Elementor utrzymuje kompatybilność wsteczną z handlerami deprekacji do 8 głównych wersji. Ale lepiej zaktualizować kod teraz, zanim notyfikacje o deprekacji zamienią się w błędy krytyczne.
Checklista migracji:
- **Metody bez **
_: wszędzie, gdzie widzą Państwo_register_controlsi_content_template, proszę usunąć prefiks. - Hook rejestracji:
elementor/widgets/widgets_registered→elementor/widgets/register. - Metoda rejestracji:
$manager->register_widget_type()→$manager->register(). - Ikony:
fa fa-*→eicon-*(natywny zestaw Elementora) lubdashicons-*(zestaw WordPressa). - Typografia:
Scheme_Typography→ albo style globalne przez'global' => [...], albo bezpośrednie wartości bez kluczascheme.
Po zmianach proszę przetestować widget na trzech poziomach: czy panel z kontrolkami otwiera się w edytorze, czy działa podgląd live (szablon Backbone), czy frontend renderuje się bez błędów. I proszę pamiętać o dry-run przed wdrożeniem na produkcję: Elementor po cichu pomija uszkodzone kontrolki, nie wywracając całej strony, dlatego wizualna weryfikacja jest obowiązkowa.
Stary kod w functions.php czy wtyczka od zera: co wybrać
Jeśli zaczynają Państwo nowy projekt, tylko wtyczka. Argumentów „za" functions.php nie zostało: nawet dla mikro-widgeta z 20 linijkami struktura wtyczki zwraca się przy pierwszej aktualizacji motywu.
Jeśli mają już Państwo działający kod w motywie, proszę wydzielić go do wtyczki podczas najbliższego refaktoringu. Proces jest prosty: proszę utworzyć folder i główny plik wtyczki według struktury powyżej, przenieść klasę widgeta do widgets/, zarejestrować przez elementor/widgets/register i przetestować na środowisku stagingowym. W praktyce zajmuje to 15-20 minut dla typowego widgeta.
Proszę zacząć od klasy bazowej z tego poradnika, skopiować główny plik i plugin.php jako szkielet, zastąpić nazwę widgeta własną. A gdy pojawią się pytania, proszę zajrzeć do sekcji Widgets na developers.elementor.com: omówiono tam renderowanie mediów, pracę z polami typu repeater i optymalizację wyjścia.



