
🛠️ Widgets personalizados para o Elementor: ciclo completo do plugin aos controlos
A atualização do tema partiu o bloco personalizado que mostrava as promoções na página inicial. Parece-lhe familiar? O código vive no functions.php, os estilos estão espalhados pelo style.css e o handler de JS está pregado ao footer.php. Mude de tema ou faça uma atualização grande do Elementor e terá de reconstruir tudo isto do zero.
O problema não é «o código ser mau». O problema é que ele vive no sítio errado. As modificações personalizadas enfiadas no tema ficam permanentemente ligadas a ele: transferir para outro site é impossível, depurar conflitos é doloroso e o carregamento condicional de assets é inexistente.
A abordagem correta é empacotar o widget personalizado como um plugin separado. É exatamente isso que a documentação oficial do Elementor recomenda para qualquer personalização não trivial. Isolamento do código, independência do tema, versionamento adequado e carregamento de JS apenas nas páginas com o widget. Neste guia, o ciclo completo: da estrutura do plugin a um widget funcional com edição inline, na API atual do Elementor (3.x/4.x).

💡 Resumo rápido:
- Registar um plugin separado para o widget, isolar o código do tema de uma vez por todas.
- Construir a classe principal com verificações de compatibilidade: Elementor ativo, versão não inferior a 3.5, PHP a partir da 7.4.
- Criar a classe do widget: estender
Widget_Base, definirregister_controls()e o método de renderização. - Ligar a edição de texto inline através de
add_inline_editing_attributes()e de um template Backbone. - Adicionar campos personalizados: seletor de imagem da biblioteca de media, dropdowns, grupos de botões, tipografia.
- Engatar o handler de JavaScript em
elementor/frontend/init, o ponto de entrada para qualquer lógica do lado do cliente.
Estrutura do plugin e classe principal
Vamos criar o plugin Elementor Awesomesauce. Estrutura mínima de ficheiros:
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
Ficheiro principal do plugin, cabeçalho padrão do WordPress mais uma classe loader com verificações de compatibilidade. Esta é exatamente a estrutura descrita no guia do primeiro addon do Elementor. O código abaixo funciona no Elementor 3.5+ e no Elementor 4.x: métodos sem o prefixo _ obsoleto, constantes para versões modernas do PHP.
Adicione este código ao elementor-awesomesauce.php. O ficheiro deve estar em /wp-content/plugins/elementor-awesomesauce/. Antes de ativar, faça um backup completo do 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 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();
O que é importante aqui. A constante MINIMUM_ELEMENTOR_VERSION, 3.5.0. A partir desta versão, entraram em vigor novas convenções para a nomenclatura de hooks e métodos sem o prefixo _. Para o Elementor 4.x (versão atual em junho de 2026) este mesmo código funciona sem alterações, a API de widgets no 4.x não foi quebrada. PHP, mínimo 7.4, mas na prática o 8.x já é o padrão de facto. A classe é declarada final: este é o ponto de entrada, não há necessidade de herdar dela.
Classe do plugin: singleton e registo do widget
Ficheiro plugin.php, um singleton que carrega os assets JS e regista o widget através do hook elementor/widgets/register. Esta é a principal mudança em relação aos guias antigos: o hook elementor/widgets/widgets_registered foi declarado obsoleto desde a versão 3.5.0, o método register_widget_type() foi substituído por 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 aceita um parâmetro $widgets_manager. No novo hook, o gestor é passado diretamente, não é obtido através de ElementorPlugin::instance()->widgets_manager. Mais limpo e sem o import extra.
Classe do widget: herança, controlos e renderização
Ficheiro widgets/awesomesauce.php, o coração do plugin. Estender Widget_Base, definir nome, título, ícone e categoria. O método register_controls() adiciona três campos de texto: linha única, multilinha e WYSIWYG. O método render() produz o markup no frontend e content_template() define o template Backbone para a pré-visualização ao vivo no 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 }
O que mudou relativamente aos guias desatualizados. Os métodos register_controls() e content_template() são escritos sem o prefixo _, esta é uma alteração desde o Elementor 3.1. Para a saída em render(), utilize print_render_attribute_string() e print_unescaped_setting() em vez de echo direto, uma abordagem moderna recomendada desde o Elementor 3.x e que funciona na 4.x. O ícone foi substituído de fa fa-pencil (Font Awesome 4, removido do núcleo) para eicon-pencil do conjunto nativo do Elementor.
Campos personalizados: media, seletores, tipografia
Os campos de texto não limitam as possibilidades. Vamos abordar quatro tipos de controlo que cobrem a maioria dos cenários reais.
Campo de media
Controls_Manager::MEDIA adiciona a seleção de imagem padrão da biblioteca de media do WordPress. O método Utils::get_placeholder_image_src() funciona nas versões atuais e fornece um placeholder cinzento se nenhuma imagem for selecionada.
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 suspensa
Controls_Manager::SELECT, escolha a partir de valores predefinidos. Abaixo está um exemplo para a tag HTML de cabeçalho:
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 botões
Controls_Manager::CHOOSE exibe uma linha de ícones para seleção visual. Ícones, apenas do conjunto eicon-*, não 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 através de Group Control
O group control de tipografia fornece o conjunto completo: tipo de letra, tamanho, espaçamento entre letras, peso, tudo com pontos de quebra responsivos. Mais detalhes na documentação do Group_Control_Typography. Ponto-chave: Scheme_Typography foi descontinuado desde o Elementor 3.x, não o utilize. Em vez disso, ligue a estilos globais ou omita completamente a chave 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 );
Porquê um plugin e não o functions.php
Um widget personalizado no tema funciona. Mas apenas até mudar de tema. Num plugin separado obtém três vantagens importantes:
- Carregamento condicional. O Elementor chama
widget_scriptsapenas quando o widget é realmente exibido na página, e não em todo o site. Para projetos com dezenas de widgets, isto representa uma poupança significativa em pedidos HTTP. - Isolamento. A lógica PHP, o CSS e o JS não se misturam com o tema. Encontrar e corrigir um bug demora minutos, não horas a escavar 2000 linhas no
functions.php. - Portabilidade. Ative o plugin noutro site e o widget funciona. Sem copiar e colar e editar caminhos manualmente.

Vídeo: demonstração ao vivo de um plugin vazio a um widget funcional
A teoria é boa, mas ver o código em ação é mais rápido. Neste guia de 30 minutos, o autor percorre todo o processo: de uma pasta vazia a um widget com controlos e renderização.
⁉️🤔 Perguntas frequentes
Porque é que o widget não aparece no painel do Elementor?
Primeiro, verifique o hook de registo. Desde o Elementor 3.5, utiliza-se
elementor/widgets/register(e nãoelementor/widgets/widgets_registered, que está obsoleto). Segundo: o métodoregister_controls()deve estar sem o prefixo_. Terceiro:get_categories()deve devolver um array com uma categoria existente,'general'funciona sempre. Quarto: limpe a cache do WordPress após ativar o plugin.
Qual é a diferença entre print_render_attribute_string() e o echo direto no render()?
O
print_render_attribute_string()aplica automaticamente filtros de atributos, incluindo edição inline e atributos de dados do Elementor. Umechodireto não fornecerá estes dados, a edição inline simplesmente não será ativada. Para imprimir valores de configuração, utilizeprint_unescaped_setting(), que trata corretamente do escape dentro dos controlos.
É possível funcionar sem um ficheiro JS separado?
Sim, se o widget se limitar a renderizar markup PHP sem interatividade. Mas assim que surgirem sliders, animações, carregamento AJAX ou qualquer dinâmica do lado do cliente, o JS é obrigatório. Mesmo um handler mínimo (como no exemplo acima) fornece um ponto de entrada para lógica futura, sem exigir a reescrita do registo mais tarde.
O namespace é obrigatório no plugin?
Formalmente, não. Mas sem um namespace, arrisca-se a ter conflitos de nomes de classes com outro plugin ou tema; nomes como
PluginouWidgetestão longe de ser únicos. Para produção, é obrigatório. O prefixoElementorAwesomesauceWidgetsgarante praticamente a unicidade.
Como atualizar um widget antigo escrito segundo guias desatualizados?
O plano de migração está descrito no guia oficial de depreciações do Elementor: (1) renomeie os métodos com o prefixo
_,_register_controls()→register_controls(),_content_template()→content_template(); (2) substitua o hook porelementor/widgets/registere o método de registo por$manager->register(); (3) substitua os íconesfa fa-*poreicon-*, removaScheme_Typography. Após as edições, aumente aMINIMUM_ELEMENTOR_VERSIONpara a atual e teste o widget a três níveis: painel do editor, pré-visualização ao vivo e frontend.
O que fazer com widgets antigos: plano de migração para a API atual
Se já tem widgets personalizados escritos segundo guias de 2019-2021, não entre em pânico. O Elementor mantém compatibilidade retroativa com manipuladores de depreciação até 8 versões principais. Mas é melhor atualizar o código agora, antes que os avisos de depreciação se transformem em erros fatais.
Lista de verificação da migração:
- **Métodos sem **
_: em todo o lado onde vir_register_controlse_content_template, remova o prefixo. - Hook de registo:
elementor/widgets/widgets_registered→elementor/widgets/register. - Método de registo:
$manager->register_widget_type()→$manager->register(). - Ícones:
fa fa-*→eicon-*(conjunto nativo do Elementor) oudashicons-*(conjunto do WordPress). - Tipografia:
Scheme_Typography→ ou estilos globais através de'global' => [...], ou valores diretos sem a chavescheme.
Após as alterações, teste o widget a três níveis: o painel de controlos abre no editor, a pré-visualização ao vivo funciona (template Backbone), o frontend renderiza sem erros. E lembre-se do teste em seco antes de implementar em produção: o Elementor ignora silenciosamente controlos quebrados sem fazer crash da página inteira, pelo que a verificação visual é obrigatória.
Código antigo no functions.php ou plugin de raiz: o que escolher
Se está a começar um novo projeto, apenas um plugin. Já não há argumentos "a favor" do functions.php: mesmo para um micro-widget de 20 linhas, a estrutura de plugin compensa com a primeira atualização do tema.
Se já tem código funcional no tema, extraia-o para um plugin na próxima refatoração. O processo é simples: crie uma pasta e o ficheiro principal do plugin de acordo com a estrutura acima, mova a classe do widget para widgets/, configure o registo através de elementor/widgets/register e teste em staging. Na prática, isto demora 15 a 20 minutos para um widget típico.
Comece com a classe base deste guia, copie o ficheiro principal e o plugin.php como esqueleto, substitua o nome do widget pelo seu. E quando surgirem dúvidas, consulte a secção de Widgets em developers.elementor.com: aborda a renderização de media, o trabalho com campos repetidores e a otimização da saída.



