Skip to content

Tout pour WordPress, le développement web — et plus encore

🛠️ Widgets Elementor personnalisés : cycle complet du plugin aux contrôles

🛠️ Widgets Elementor personnalisés : cycle complet du plugin aux contrôles

La mise à jour du thème a cassé le bloc personnalisé qui affichait les promotions sur la page d’accueil. Cela vous rappelle quelque chose? Le code réside dans functions.php, les styles sont éparpillés dans style.css et le gestionnaire JS est cloué dans footer.php. Changez de thème ou faites une mise à jour majeure d’Elementor, et vous devez reconstituer tout cela à partir de zéro.

Le problème n’est pas que «le code est mauvais». Le problème est qu’il réside au mauvais endroit. Les modifications personnalisées entassées dans le thème y sont liées de manière permanente: le transfert vers un autre site est impossible, le débogage des conflits est pénible et le chargement conditionnel des assets est inexistant.

La bonne approche consiste à empaqueter le widget personnalisé sous forme de plugin séparé. C’est exactement ce que recommande la documentation officielle d’Elementor pour toute personnalisation non triviale. Isolation du code, indépendance vis-à-vis du thème, versionnement correct et chargement du JS uniquement sur les pages où le widget est présent. Dans ce guide, le cycle complet: de la structure du plugin à un widget fonctionnel avec édition en ligne, sur l’API Elementor actuelle (3.x/4.x).

Éditeurs de code sur un écran d'ordinateur

💡 Aperçu rapide:

  • Enregistrez un plugin séparé pour le widget, isolez le code du thème une fois pour toutes.
  • Construisez la classe principale avec des vérifications de compatibilité: Elementor actif, version 3.5 minimum, PHP à partir de 7.4.
  • Créez la classe du widget: étendez Widget_Base, définissez register_controls() et la méthode de rendu.
  • Mettez en place l’édition de texte en ligne via add_inline_editing_attributes() et un template Backbone.
  • Ajoutez des champs personnalisés: sélecteur d’image depuis la médiathèque, listes déroulantes, groupes de boutons, typographie.
  • Accrochez le gestionnaire JavaScript à elementor/frontend/init, le point d’entrée pour toute logique côté client.

Structure du plugin et classe principale

Nous allons créer le plugin Elementor Awesomesauce. Structure de fichiers minimale:

1elementor-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

Fichier principal du plugin, en-tête WordPress standard plus une classe de chargement avec vérifications de compatibilité. C’est exactement la structure décrite dans le guide du premier addon Elementor. Le code ci-dessous fonctionne sous Elementor 3.5+ et Elementor 4.x: des méthodes sans le préfixe obsolète _, des constantes pour les versions modernes de PHP.

Ajoutez ce code à elementor-awesomesauce.php. Le fichier doit se trouver dans /wp-content/plugins/elementor-awesomesauce/. Avant l’activation, faites une sauvegarde complète du site.

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
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();

Ce qui est important ici. La constante MINIMUM_ELEMENTOR_VERSION, 3.5.0. À partir de cette version, de nouvelles conventions pour le nommage des hooks et des méthodes sans le préfixe _ sont entrées en vigueur. Pour Elementor 4.x (version actuelle en juin 2026), ce même code fonctionne sans modification, l’API des widgets en 4.x n’a pas été cassée. PHP, minimum 7.4, mais en pratique la version 8.x est déjà le standard de fait. La classe est déclarée final: c’est le point d’entrée, pas besoin d’en hériter.

Classe du plugin: singleton et enregistrement du widget

Fichier plugin.php, un singleton qui charge les assets JS et enregistre le widget via le hook elementor/widgets/register. C’est le principal changement par rapport aux anciens guides: le hook elementor/widgets/widgets_registered a été déclaré obsolète depuis la version 3.5.0, la méthode register_widget_type() a été remplacée par 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();

Remarque: register_widgets accepte un paramètre $widgets_manager. Dans le nouveau hook, le manager est passé directement, et non récupéré via ElementorPlugin::instance()->widgets_manager. Plus propre et sans import supplémentaire.

Classe du widget: héritage, contrôles et rendu

Fichier widgets/awesomesauce.php, le cœur du plugin. Étendez Widget_Base, définissez le nom, le titre, l’icône et la catégorie. La méthode register_controls() ajoute trois champs texte: ligne simple, multiligne et WYSIWYG. La méthode render() produit le balisage en frontend, et content_template() définit le template Backbone pour l’aperçu en direct dans l’éditeur.

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' => __( '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}

Ce qui a changé par rapport aux anciens guides. Les méthodes register_controls() et content_template() s’écrivent sans le préfixe _, c’est un changement depuis Elementor 3.1. Pour l’affichage dans render(), utilisez print_render_attribute_string() et print_unescaped_setting() au lieu d’un echo direct, une approche moderne recommandée depuis Elementor 3.x et qui fonctionne en 4.x. L’icône est remplacée: on passe de fa fa-pencil (Font Awesome 4, retirée du cœur) à eicon-pencil, issue du jeu d’icônes natif d’Elementor.

Champs personnalisés: média, listes déroulantes, typographie

Les champs texte ne limitent pas les possibilités. Nous allons voir quatre types de contrôles qui couvrent la plupart des cas réels.

Champ média

Controls_Manager::MEDIA ajoute la sélection d’image standard depuis la bibliothèque de médias WordPress. La méthode Utils::get_placeholder_image_src() fonctionne dans les versions actuelles et fournit une image grise de substitution si aucune image n’est sélectionnée.

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);

Liste déroulante

Controls_Manager::SELECT, choix parmi des valeurs prédéfinies. Voici un exemple pour la balise HTML de titre:

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);

Groupe de boutons

Controls_Manager::CHOOSE affiche une rangée d’icônes pour une sélection visuelle. Les icônes, uniquement issues du jeu eicon-*, pas de 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);

Typographie via un contrôle de groupe

Le contrôle de groupe typographie fournit l’ensemble complet: police, taille, espacement des lettres, graisse, le tout avec des points de rupture responsives. Plus de détails dans la documentation de Group_Control_Typography. Point clé: Scheme_Typography est déprécié depuis Elementor 3.x, ne l’utilisez pas. À la place, liez le contrôle aux styles globaux ou omettez purement et simplement la clé 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);

Pourquoi un plugin et pas le fichier functions.php

Un widget personnalisé dans le thème fonctionne. Mais seulement jusqu’à ce que vous changiez de thème. Dans un plugin séparé, vous obtenez trois avantages importants:

  • Chargement conditionnel. Elementor appelle widget_scripts uniquement lorsque le widget est effectivement affiché sur la page, et non sur l’ensemble du site. Pour les projets comportant des dizaines de widgets, cela représente une économie notable en requêtes HTTP.
  • Isolation. La logique PHP, le CSS et le JS ne se mélangent pas avec le thème. Trouver et corriger un bug prend quelques minutes, pas des heures à fouiller dans 2000 lignes de functions.php.
  • Portabilité. Activez le plugin sur un autre site, le widget fonctionne. Pas de copier-coller ni de modification manuelle des chemins.
Développeur à un bureau avec deux écrans

Vidéo: démonstration en direct, du plugin vide au widget fonctionnel

La théorie, c’est bien, mais voir le code en action est plus rapide. Dans ce guide de 30 minutes, l’auteur parcourt l’intégralité du processus: d’un dossier vide à un widget avec contrôles et rendu.

⁉️🤔 Foire aux questions

Pourquoi le widget n’apparaît-il pas dans le panneau Elementor?

Vérifiez d’abord le hook d’enregistrement. Depuis Elementor 3.5, on utilise elementor/widgets/register (et non elementor/widgets/widgets_registered, qui est déprécié). Deuxièmement: la méthode register_controls() ne doit pas comporter le préfixe _. Troisièmement: get_categories() doit retourner un tableau contenant une catégorie existante, 'general' fonctionne toujours. Quatrièmement: videz le cache WordPress après avoir activé le plugin.

Quelle est la différence entre print_render_attribute_string() et un echo direct dans render()?

print_render_attribute_string() applique automatiquement les filtres d’attributs, y compris l’édition en ligne et les attributs de données Elementor. Un echo direct ne fournira pas ces données, l’édition en ligne ne s’activera tout simplement pas. Pour afficher les valeurs des réglages, utilisez print_unescaped_setting(), qui gère correctement l’échappement à l’intérieur des contrôles.

Peut-on se passer d’un fichier JS séparé?

Oui, si le widget se contente de produire du balisage PHP sans interactivité. Mais dès que des sliders, des animations, du chargement AJAX ou toute dynamique côté client entrent en jeu, le JS devient obligatoire. Même un gestionnaire minimal (comme dans l’exemple ci-dessus) offre un point d’entrée pour de futures logiques, sans avoir à réécrire l’enregistrement par la suite.

Un namespace est-il obligatoire dans le plugin?

Formellement, non. Mais sans namespace, vous risquez des conflits de noms de classe avec un autre plugin ou thème: des noms comme Plugin ou Widget sont loin d’être uniques. En production, c’est obligatoire. Le préfixe ElementorAwesomesauceWidgets garantit pratiquement l’unicité.

Comment mettre à jour un ancien widget écrit selon des tutoriels obsolètes?

Le plan de migration est décrit dans le guide officiel des dépréciations Elementor: (1) renommez les méthodes avec le préfixe _, _register_controls()register_controls(), _content_template()content_template(); (2) remplacez le hook par elementor/widgets/register, et la méthode d’enregistrement par $manager->register(); (3) remplacez les icônes fa fa-* par eicon-*, supprimez Scheme_Typography. Après les modifications, passez MINIMUM_ELEMENTOR_VERSION à la version actuelle et testez le widget sur trois niveaux: panneau de l’éditeur, aperçu en direct, frontend.

Que faire des anciens widgets: plan de migration vers l’API actuelle

Si vous avez déjà des widgets personnalisés écrits selon des tutoriels de 2019-2021, pas de panique. Elementor maintient une compatibilité ascendante avec des gestionnaires de dépréciation jusqu’à 8 versions majeures. Mais il est préférable de mettre le code à jour maintenant, avant que les avis de dépréciation ne se transforment en erreurs fatales.

Checklist de migration:

  • Méthodes sans _: partout où vous voyez _register_controls et _content_template, supprimez le préfixe.
  • Hook d’enregistrement: elementor/widgets/widgets_registeredelementor/widgets/register.
  • Méthode d’enregistrement: $manager->register_widget_type()$manager->register().
  • Icônes: fa fa-*eicon-* (jeu natif Elementor) ou dashicons-* (jeu WordPress).
  • Typographie: Scheme_Typography → soit des styles globaux via 'global' => [...], soit des valeurs directes sans la clé scheme.

Après les modifications, testez le widget sur trois niveaux: le panneau des contrôles s’ouvre-t-il dans l’éditeur, l’aperçu en direct fonctionne-t-il (template Backbone), le frontend s’affiche-t-il sans erreur. Et n’oubliez pas le dry-run avant de déployer en production: Elementor ignore silencieusement les contrôles cassés sans faire planter toute la page, une vérification visuelle est donc indispensable.

Ancien code dans functions.php ou plugin from scratch: que choisir

Si vous démarrez un nouveau projet, uniquement un plugin. Il n’y a plus aucun argument en faveur du functions.php: même pour un micro-widget de 20 lignes, la structure en plugin est rentabilisée dès la première mise à jour du thème.

Si vous avez déjà du code fonctionnel dans le thème, extrayez-le dans un plugin lors de la prochaine refactorisation. Le processus est simple: créez un dossier et un fichier principal de plugin selon la structure ci-dessus, déplacez la classe du widget dans widgets/, configurez l’enregistrement via elementor/widgets/register, et testez sur un environnement de staging. En pratique, cela prend 15 à 20 minutes pour un widget typique.

Commencez par la classe de base de ce guide, copiez le fichier principal et plugin.php comme squelette, remplacez le nom du widget par le vôtre. Et si des questions surviennent, consultez la section Widgets sur developers.elementor.com: elle couvre le rendu des médias, le travail avec les champs repeater et l’optimisation de la sortie.