Skip to content

Todo para WordPress, el desarrollo web — y mucho más

🛠️ Widgets personalizados de Elementor: ciclo completo del plugin a los controles

🛠️ Widgets personalizados de Elementor: ciclo completo del plugin a los controles

La actualización del tema rompió el bloque personalizado que mostraba promociones en la página de inicio. ¿Le suena familiar? El código vive en functions.php, los estilos están desparramados en style.css y el manejador JS está clavado en footer.php. Cambie de tema o haga una actualización mayor de Elementor y tendrá que reensamblar todo esto desde cero.

El problema no es que «el código sea malo». El problema es que reside en el lugar equivocado. Las modificaciones personalizadas incrustadas en el tema quedan atadas a él de forma permanente: transferirlas a otro sitio es imposible, depurar conflictos es doloroso y la carga condicional de recursos es inexistente.

El enfoque correcto es empaquetar el widget personalizado como un plugin independiente. Es exactamente lo que la documentación oficial de Elementor recomienda para cualquier personalización no trivial. Aislamiento del código, independencia del tema, versionado adecuado y carga de JS solo en las páginas que contienen el widget. En esta guía, el ciclo completo: desde la estructura del plugin hasta un widget funcional con edición en línea, sobre la API actual de Elementor (3.x/4.x).

Editores de código en una pantalla de ordenador

💡 Resumen rápido:

  • Registre un plugin independiente para el widget, aísle el código del tema de una vez por todas.
  • Construya la clase principal con verificaciones de compatibilidad: Elementor activo, versión no inferior a 3.5, PHP desde 7.4.
  • Cree la clase del widget: extienda Widget_Base, defina register_controls() y el método de renderizado.
  • Conecte la edición de texto en línea mediante add_inline_editing_attributes() y una plantilla Backbone.
  • Añada campos personalizados: selector de imagen desde la biblioteca de medios, listas desplegables, grupos de botones, tipografía.
  • Enganche el manejador JavaScript a elementor/frontend/init, el punto de entrada para cualquier lógica del lado del cliente.

Estructura del plugin y clase principal

Crearemos el plugin Elementor Awesomesauce. Estructura mínima de archivos:

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

Archivo principal del plugin, cabecera estándar de WordPress más una clase cargadora con verificaciones de compatibilidad. Esta es exactamente la estructura descrita en la primera guía de addons de Elementor. El código a continuación funciona en Elementor 3.5+ y Elementor 4.x: métodos sin el prefijo obsoleto _, constantes para versiones modernas de PHP.

Añada este código a elementor-awesomesauce.php. El archivo debe estar en /wp-content/plugins/elementor-awesomesauce/. Antes de activarlo, haga una copia de seguridad completa del sitio.

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

Lo importante aquí. La constante MINIMUM_ELEMENTOR_VERSION, 3.5.0. A partir de esta versión entraron en vigor nuevas convenciones para la nomenclatura de hooks y métodos sin el prefijo _. Para Elementor 4.x (versión actual a junio de 2026) este mismo código funciona sin cambios, la API de widgets en 4.x no se rompió. PHP, mínimo 7.4, aunque en la práctica 8.x ya es el estándar de facto. La clase se declara final: es el punto de entrada, no hay necesidad de heredar de ella.

Clase del plugin: singleton y registro del widget

Archivo plugin.php, un singleton que carga los recursos JS y registra el widget a través del hook elementor/widgets/register. Este es el cambio principal respecto a las guías antiguas: el hook elementor/widgets/widgets_registered se declaró obsoleto desde la versión 3.5.0, el método register_widget_type() fue reemplazado por 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();

Nota: register_widgets acepta un parámetro $widgets_manager. En el nuevo hook, el gestor se pasa directamente, no se obtiene mediante ElementorPlugin::instance()->widgets_manager. Más limpio y sin la importación adicional.

Clase del widget: herencia, controles y renderizado

Archivo widgets/awesomesauce.php, el corazón del plugin. Extienda Widget_Base, defina nombre, título, icono y categoría. El método register_controls() añade tres campos de texto: una línea, multilínea y WYSIWYG. El método render() genera el marcado en el frontend, y content_template() define la plantilla Backbone para la vista previa en vivo en el editor.

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}

Qué cambió respecto a las guías desactualizadas. Los métodos register_controls() y content_template() se escriben sin el prefijo _, este es un cambio desde Elementor 3.1. Para la salida en render(), use print_render_attribute_string() y print_unescaped_setting() en lugar de echo directo, un enfoque moderno recomendado desde Elementor 3.x y que funciona en 4.x. El ícono se reemplazó de fa fa-pencil (Font Awesome 4, eliminado del núcleo) a eicon-pencil del conjunto nativo de Elementor.

Campos personalizados: medios, selects, tipografía

Los campos de texto no limitan las posibilidades. Cubriremos cuatro tipos de control que manejan la mayoría de los escenarios reales.

Campo de medios

Controls_Manager::MEDIA agrega la selección de imágenes estándar desde la biblioteca de medios de WordPress. El método Utils::get_placeholder_image_src() funciona en las versiones actuales y proporciona un marcador de posición gris si no se selecciona ninguna imagen.

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 desplegable

Controls_Manager::SELECT, elección entre valores predefinidos. A continuación, un ejemplo para la etiqueta HTML de encabezado:

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

Grupo de botones

Controls_Manager::CHOOSE muestra una fila de íconos para la selección visual. Íconos, solo del conjunto eicon-*, no 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);

Tipografía mediante control de grupo

El control de grupo de tipografía proporciona el conjunto completo: fuente, tamaño, espaciado entre letras, peso, todo con puntos de quiebre responsivos. Más detalles en la documentación de Group_Control_Typography. Punto clave: Scheme_Typography quedó obsoleto desde Elementor 3.x, no lo use. En su lugar, vincule a estilos globales u omita la clave scheme por completo.

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

Por qué un plugin y no functions.php

Un widget personalizado en el tema funciona. Pero solo hasta que cambie de tema. En un plugin separado obtiene tres ventajas importantes:

  • Carga condicional. Elementor llama a widget_scripts solo cuando el widget se muestra realmente en la página, no en todo el sitio. Para proyectos con docenas de widgets, esto es un ahorro notable en solicitudes HTTP.
  • Aislamiento. La lógica PHP, el CSS y el JS no se mezclan con el tema. Encontrar y corregir un error lleva minutos, no horas de escarbar entre 2000 líneas en functions.php.
  • Portabilidad. Active el plugin en otro sitio y el widget funciona. Sin copiar y pegar ni editar rutas manualmente.
Desarrollador en un escritorio con dos monitores

Video: demostración en vivo desde un plugin vacío hasta un widget funcional

La teoría es buena, pero ver el código en acción es más rápido. En esta guía de 30 minutos, el autor recorre todo el proceso: desde una carpeta vacía hasta un widget con controles y renderizado.

⁉️🤔 Preguntas frecuentes

¿Por qué no aparece el widget en el panel de Elementor?

Primero, revise el hook de registro. Desde Elementor 3.5 se usa elementor/widgets/register (no elementor/widgets/widgets_registered, está obsoleto). Segundo: el método register_controls() debe estar sin el prefijo _. Tercero: get_categories() debe devolver un array con una categoría existente, 'general' siempre funciona. Cuarto: limpie la caché de WordPress después de activar el plugin.

¿Cuál es la diferencia entre print_render_attribute_string() y el echo directo en render()?

print_render_attribute_string() aplica automáticamente filtros de atributos, incluyendo edición en línea y atributos de datos de Elementor. Un echo directo no proporcionará estos datos, la edición en línea simplemente no se activará. Para imprimir valores de configuración, use print_unescaped_setting(), maneja correctamente el escapado dentro de los controles.

¿Se puede prescindir de un archivo JS separado?

Sí, si el widget solo renderiza marcado PHP sin interactividad. Pero en cuanto aparecen sliders, animaciones, carga AJAX o cualquier dinámica de cliente, el JS es obligatorio. Incluso un manejador mínimo (como en el ejemplo anterior) proporciona un punto de entrada para lógica futura, sin requerir reescribir el registro más tarde.

¿Es obligatorio un namespace en el plugin?

Formalmente no. Pero sin un namespace corre el riesgo de tener conflictos de nombres de clase con otro plugin o tema, nombres como Plugin o Widget están lejos de ser únicos. Para producción, es obligatorio. El prefijo ElementorAwesomesauceWidgets prácticamente garantiza la unicidad.

¿Cómo actualizar un widget antiguo escrito según guías desactualizadas?

El plan de migración se describe en la guía oficial de obsolescencias de Elementor: (1) renombre los métodos con el prefijo _, _register_controls()register_controls(), _content_template()content_template(); (2) reemplace el hook por elementor/widgets/register, y el método de registro por $manager->register(); (3) reemplace los iconos fa fa-* por eicon-*, elimine Scheme_Typography. Después de las ediciones, incremente MINIMUM_ELEMENTOR_VERSION a la actual y pruebe el widget en tres niveles: panel del editor, vista previa en vivo, frontend.

Qué hacer con los widgets antiguos: plan de migración a la API actual

Si ya tiene widgets personalizados escritos según guías de 2019-2021, no se alarme. Elementor mantiene compatibilidad hacia atrás con manejadores de obsolescencia hasta 8 versiones mayores. Pero es mejor actualizar el código ahora, antes de que los avisos de obsolescencia se conviertan en errores fatales.

Lista de verificación para la migración:

  • **Métodos sin _: donde vea _register_controls y _content_template, elimine el prefijo.
  • Hook de registro: elementor/widgets/widgets_registeredelementor/widgets/register.
  • Método de registro: $manager->register_widget_type()$manager->register().
  • Iconos: fa fa-*eicon-* (conjunto nativo de Elementor) o dashicons-* (conjunto de WordPress).
  • Tipografía: Scheme_Typography → ya sea estilos globales mediante 'global' => [...], o valores directos sin la clave scheme.

Después de los cambios, pruebe el widget en tres niveles: ¿se abre el panel de controles en el editor?, ¿funciona la vista previa en vivo (plantilla Backbone)?, ¿se renderiza el frontend sin errores? Y recuerde hacer una prueba en seco antes de desplegar a producción: Elementor omite silenciosamente los controles rotos sin bloquear toda la página, por lo que la verificación visual es obligatoria.

Código antiguo en functions.php o plugin desde cero: qué elegir

Si está comenzando un nuevo proyecto, solo un plugin. No quedan argumentos «a favor» de functions.php: incluso para un micro-widget de 20 líneas, la estructura de plugin se amortiza con la primera actualización del tema.

Si ya tiene código funcionando en el tema, extráigalo a un plugin en la próxima refactorización. El proceso es sencillo: cree una carpeta y un archivo principal de plugin según la estructura anterior, mueva la clase del widget a widgets/, configure el registro mediante elementor/widgets/register y pruebe en staging. En la práctica, esto toma de 15 a 20 minutos para un widget típico.

Comience con la clase base de esta guía, copie el archivo principal y plugin.php como esqueleto, reemplace el nombre del widget por el suyo. Y cuando surjan preguntas, consulte la sección de Widgets en developers.elementor.com: cubre el renderizado de medios, el trabajo con campos repetidores y la optimización de la salida.