
🛠️ Widget Elementor personalizzati: ciclo completo dal plugin ai controlli
L’aggiornamento del tema ha rotto il blocco personalizzato che mostrava le promozioni in homepage. Ti suona familiare? Il codice sta in functions.php, gli stili sono spalmati su style.css e il gestore JS è inchiodato a footer.php. Cambi tema o fai un aggiornamento importante di Elementor e devi ricostruire tutto da zero.
Il problema non è che «il codice è fatto male». Il problema è che risiede nel posto sbagliato. Le modifiche personalizzate infilate nel tema gli sono legate in modo permanente: trasferirle su un altro sito è impossibile, fare debug dei conflitti è doloroso e il caricamento condizionale degli asset è inesistente.
L’approccio corretto è impacchettare il widget personalizzato come plugin separato. È esattamente ciò che raccomanda la documentazione ufficiale di Elementor per qualsiasi personalizzazione non banale. Isolamento del codice, indipendenza dal tema, versionamento corretto e caricamento JS solo sulle pagine dove il widget è presente. In questa guida, il ciclo completo: dalla struttura del plugin a un widget funzionante con editing inline, sulle API attuali di Elementor (3.x/4.x).

💡 Panoramica rapida:
- Registra un plugin separato per il widget, isola il codice dal tema una volta per tutte.
- Costruisci la classe principale con controlli di compatibilità: Elementor attivo, versione non inferiore alla 3.5, PHP dalla 7.4.
- Crea la classe del widget: estendi
Widget_Base, definisciregister_controls()e il metodo di render. - Collega l’editing inline del testo tramite
add_inline_editing_attributes()e un template Backbone. - Aggiungi campi personalizzati: selettore immagini dalla libreria media, dropdown, gruppi di pulsanti, tipografia.
- Aggancia il gestore JavaScript a
elementor/frontend/init, il punto di ingresso per qualsiasi logica lato client.
Struttura del plugin e classe principale
Creeremo il plugin Elementor Awesomesauce. Struttura minima dei file:
1 elementor-awesomesauce/ 2 ├── elementor-awesomesauce.php ← entry point 3 ├── plugin.php ← plugin singleton class 4 ├── widgets/ 5 │ └── awesomesauce.php ← widget class 6 └── assets/ 7 └── js/ 8 └── awesomesauce.js ← frontend JS handler
File principale del plugin, header standard di WordPress più una classe loader con controlli di compatibilità. Questa è esattamente la struttura descritta nella guida al primo addon Elementor. Il codice qui sotto funziona con Elementor 3.5+ ed Elementor 4.x: metodi senza il prefisso deprecato _, costanti per versioni PHP moderne.
Aggiungi questo codice a elementor-awesomesauce.php. Il file deve trovarsi in /wp-content/plugins/elementor-awesomesauce/. Prima di attivare, fai un backup completo del sito.
1 <?php 2 /** 3 * Plugin Name: Elementor Awesomesauce 4 * Description: Custom Elementor widget with live editing of text, images and styles. 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();
Cosa è importante qui. La costante MINIMUM_ELEMENTOR_VERSION, 3.5.0. A partire da questa versione sono entrate in vigore nuove convenzioni per la denominazione di hook e metodi senza il prefisso _. Per Elementor 4.x (versione corrente a giugno 2026) questo stesso codice funziona senza modifiche, l’API dei widget nella 4.x non è stata rotta. PHP, minimo 7.4, ma in pratica la 8.x è già lo standard di fatto. La classe è dichiarata final: è il punto di ingresso, non serve ereditare da essa.
Classe del plugin: singleton e registrazione del widget
File plugin.php, un singleton che carica gli asset JS e registra il widget tramite l’hook elementor/widgets/register. Questa è la differenza principale rispetto alle vecchie guide: l’hook elementor/widgets/widgets_registered è stato dichiarato deprecato dalla versione 3.5.0, il metodo register_widget_type() è stato sostituito con 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();
Nota: register_widgets accetta un parametro $widgets_manager. Nel nuovo hook, il manager viene passato direttamente, non recuperato tramite ElementorPlugin::instance()->widgets_manager. Più pulito e senza l’import extra.
Classe del widget: ereditarietà, controlli e render
File widgets/awesomesauce.php, il cuore del plugin. Estendi Widget_Base, definisci nome, titolo, icona e categoria. Il metodo register_controls() aggiunge tre campi di testo: riga singola, multilinea e WYSIWYG. Il metodo render() produce il markup nel frontend, e content_template() definisce il template Backbone per l’anteprima live nell’editor.
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' => __( 'Title', 'elementor-awesomesauce' ), 45 ] 46 ); 47 48 $this->add_control( 49 'description', 50 [ 51 'label' => __( 'Description', 'elementor-awesomesauce' ), 52 'type' => Controls_Manager::TEXTAREA, 53 'default' => __( 'Description', 'elementor-awesomesauce' ), 54 ] 55 ); 56 57 $this->add_control( 58 'content', 59 [ 60 'label' => __( 'Content', 'elementor-awesomesauce' ), 61 'type' => Controls_Manager::WYSIWYG, 62 'default' => __( 'Content', '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 }
Cosa è cambiato rispetto alle guide obsolete. I metodi register_controls() e content_template() si scrivono senza il prefisso _, una modifica introdotta con Elementor 3.1. Per l'output in render(), usa print_render_attribute_string() e print_unescaped_setting() invece di un echo diretto, un approccio moderno raccomandato da Elementor 3.x e funzionante nella 4.x. L'icona è stata sostituita da fa fa-pencil (Font Awesome 4, rimosso dal core) a eicon-pencil del set nativo di Elementor.
Campi personalizzati: media, select, tipografia
I campi di testo non esauriscono le possibilità. Vediamo quattro tipi di controllo che coprono la maggior parte degli scenari reali.
Campo media
Controls_Manager::MEDIA aggiunge la selezione standard delle immagini dalla libreria media di WordPress. Il metodo Utils::get_placeholder_image_src() funziona nelle versioni correnti e fornisce un placeholder grigio se non viene selezionata alcuna immagine.
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 a discesa
Controls_Manager::SELECT, scelta tra valori predefiniti. Di seguito un esempio per il tag HTML dell'intestazione:
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 );
Gruppo di pulsanti
Controls_Manager::CHOOSE mostra una fila di icone per una selezione visiva. Solo icone del set eicon-*, non 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 );
Tipografia tramite Group Control
Il group control per la tipografia fornisce il set completo: font, dimensione, spaziatura delle lettere, peso, il tutto con breakpoint responsive. Maggiori dettagli nella documentazione di Group_Control_Typography. Punto chiave: Scheme_Typography è deprecato da Elementor 3.x, non usarlo. Al suo posto, collega il controllo agli stili globali oppure ometti del tutto la chiave 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 );
Perché un plugin e non functions.php
Un widget personalizzato nel tema funziona. Ma solo finché non cambi tema. In un plugin separato ottieni tre vantaggi importanti:
- Caricamento condizionale. Elementor chiama
widget_scriptssolo quando il widget è effettivamente visualizzato nella pagina, non sull'intero sito. Per progetti con decine di widget, questo comporta un risparmio notevole di richieste HTTP. - Isolamento. La logica PHP, i CSS e il JS non si mescolano con il tema. Trovare e correggere un bug richiede minuti, non ore passate a scavare tra 2000 righe di
functions.php. - Portabilità. Attivi il plugin su un altro sito e il widget funziona. Niente copia-incolla e modifiche manuali dei percorsi.

Video: dimostrazione live da plugin vuoto a widget funzionante
La teoria è utile, ma vedere il codice in azione è più rapido. In questa guida di 30 minuti, l'autore percorre l'intero processo: da una cartella vuota a un widget con controlli e render.
⁉️🤔 Domande frequenti
Perché il widget non compare nel pannello di Elementor?
Per prima cosa, verifica l'hook di registrazione. Da Elementor 3.5 si usa
elementor/widgets/register(nonelementor/widgets/widgets_registered, che è deprecato). Secondo: il metodoregister_controls()deve essere senza il prefisso_. Terzo:get_categories()deve restituire un array con una categoria esistente,'general'funziona sempre. Quarto: svuota la cache di WordPress dopo aver attivato il plugin.
Qual è la differenza tra print_render_attribute_string() e l'echo diretto in render()?
print_render_attribute_string()applica automaticamente i filtri sugli attributi, inclusi l'editing inline e gli attributi dati di Elementor. Unechodiretto non fornirà questi dati, l'editing inline semplicemente non si attiverà. Per emettere i valori delle impostazioni, usaprint_unescaped_setting(), che gestisce correttamente l'escape all'interno dei controlli.
Si può fare a meno di un file JS separato?
Sì, se il widget si limita a generare markup PHP senza interattività. Ma non appena compaiono slider, animazioni, caricamenti AJAX o qualsiasi dinamica lato client, il JS è obbligatorio. Anche un handler minimo (come nell'esempio sopra) fornisce un punto di ingresso per logiche future, senza dover riscrivere la registrazione in seguito.
Un namespace è obbligatorio nel plugin?
Formalmente no. Ma senza namespace rischi conflitti di nomi di classe con un altro plugin o tema, nomi come
PluginoWidgetsono tutt'altro che unici. Per la produzione, è obbligatorio. Il prefissoElementorAwesomesauceWidgetsgarantisce praticamente l'unicità.
Come aggiornare un vecchio widget scritto secondo guide obsolete?
Il piano di migrazione è descritto nella guida ufficiale alle deprecazioni di Elementor: (1) rinomina i metodi con il prefisso
_,_register_controls()→register_controls(),_content_template()→content_template(); (2) sostituisci l'hook conelementor/widgets/register, e il metodo di registrazione con$manager->register(); (3) sostituisci le iconefa fa-*coneicon-*, rimuoviScheme_Typography. Dopo le modifiche, aggiornaMINIMUM_ELEMENTOR_VERSIONalla versione corrente e testa il widget su tre livelli: pannello editor, anteprima live, frontend.
Cosa fare con i vecchi widget: piano di migrazione all'API attuale
Se hai già widget personalizzati scritti secondo guide del 2019-2021, niente panico. Elementor mantiene la retrocompatibilità con gestori di deprecazione fino a 8 versioni major. Ma è meglio aggiornare il codice ora, prima che gli avvisi di deprecazione diventino errori fatali.
Checklist di migrazione:
- Metodi senza
_: ovunque vedi_register_controlse_content_template, rimuovi il prefisso. - Hook di registrazione:
elementor/widgets/widgets_registered→elementor/widgets/register. - Metodo di registrazione:
$manager->register_widget_type()→$manager->register(). - Icone:
fa fa-*→eicon-*(set nativo Elementor) odashicons-*(set WordPress). - Tipografia:
Scheme_Typography→ o stili globali tramite'global' => [...], oppure valori diretti senza la chiavescheme.
Dopo le modifiche, testa il widget su tre livelli: il pannello dei controlli si apre nell'editor, l'anteprima live funziona (template Backbone), il frontend viene renderizzato senza errori. E ricorda il dry-run prima del deploy in produzione: Elementor salta silenziosamente i controlli danneggiati senza mandare in crash l'intera pagina, quindi la verifica visiva è obbligatoria.
Vecchio codice in functions.php o plugin da zero: cosa scegliere
Se inizi un nuovo progetto, solo plugin. Non ci sono più argomenti "a favore" di functions.php: anche per un micro-widget di 20 righe, la struttura a plugin si ripaga al primo aggiornamento del tema.
Se hai già codice funzionante nel tema, estrailo in un plugin al prossimo refactoring. Il processo è semplice: crea una cartella e il file principale del plugin secondo la struttura sopra, sposta la classe del widget in widgets/, imposta la registrazione tramite elementor/widgets/register e testa in staging. In pratica, per un widget tipico ci vogliono 15-20 minuti.
Parti dalla classe base di questa guida, copia il file principale e plugin.php come scheletro, sostituisci il nome del widget con il tuo. E quando sorgono domande, consulta la sezione Widgets su developers.elementor.com: copre il rendering dei media, il lavoro con i campi repeater e l'ottimizzazione dell'output.



