
🎯 Selectores en widgets de Elementor: guía completa para desarrolladores
Por qué los desarrolladores de Elementor necesitan selectores y cómo funcionan
Cuando un usuario ajusta los parámetros de un widget en el editor, espera una respuesta instantánea en pantalla. Sin selectores, un desarrollador tendría que escribir un manejador JS para cada cambio de campo. Con selectores, todo se resuelve mediante CSS.
El parámetro selectors (y su contraparte menos conocida selectors_dictionary) se incrusta directamente en el array de la llamada a add_control(). Elementor sustituye dinámicamente los valores de los campos en reglas CSS y las envía al archivo de la entrada, algo como /wp-content/uploads/elementor/css/post-1234.css. En cuanto el usuario sale del editor, los estilos en línea desaparecen, dejando CSS generado limpio.
💡 Resumen rápido:
- Comprender la sintaxis de selectores y la tabla de marcadores de posición
- Ver ejemplos en vivo para color y tamaños
- Aprender a obtener valores de controles vecinos
- Dominar selectors_dictionary para la sustitución de declaraciones CSS
- Armar el rompecabezas de las variables CSS y los controles ocultos
Dónde se definen los selectores
Cuando usted crea un widget, cada llamada a add_control() acepta un array de configuración. Ahí es exactamente donde residen los selectors. Para los controles de grupo la sintaxis es la misma, el array se pasa dentro del registro del grupo.
Formato básico:
1 'selectors' => [ 2 '{{WRAPPER}} .my-widget-class' => 'color: {{VALUE}}', 3 ]
La clave es un selector CSS (comienza con {{WRAPPER}} para no afectar a widgets vecinos en la página). El valor es una o más declaraciones CSS con marcadores de posición dinámicos. Elementor toma el valor actual del control y lo sustituye en lugar del marcador.
El resultado se renderiza en el archivo CSS externo de la entrada, los estilos existen solo mientras el editor está abierto e inmediatamente después de guardar. Sin desorden en línea.
Tabla de variables entre llaves
No hay magia, solo buscar y reemplazar. Pero la variedad de marcadores de posición abre las puertas a construcciones bastante ingeniosas.
Para selectores (clave del array)
Marcador | Qué sustituye |
|---|---|
| Selector único de instancia del widget, por ejemplo |
| Solo el ID del widget (la parte después del guion, |
| Restringe la regla al dispositivo especificado. Con |
| Elemento activo de un control repetidor |
Para declaraciones (valor del array)
Marcador | Qué sustituye |
|---|---|
| Valor bruto del control. Puede ser sobrescrito por |
| Número y unidad de medida de controles numéricos. Generalmente van en pares: |
| Direcciones del control de dimensiones |
| Acceso a una propiedad con nombre de controles compuestos: por ejemplo, Media Control devuelve un array con los campos |
| Valor de otro control por su ID. Los sufijos |
| Valor de reserva: si el control está vacío, se sustituirá |
Ejemplos simples, del color a la imagen de fondo
Color desde la paleta. Sin nada extra:
1 'selectors' => [ 2 '{{WRAPPER}} .elementor-svg-divider-basic-text' => 'color: {{VALUE}}', 3 ],
Control numérico con y sin unidad. La segunda propiedad (stroke-width) está intencionalmente sin {{UNIT}}, el grosor del trazo está en píxeles, sin px:
1 'selectors' => [ 2 '{{WRAPPER}} svg.sde-classic' => 3 'height: {{SIZE}}{{UNIT}}; stroke-width: {{SIZE}};', 4 ],
Espaciado desde el control de dimensiones, cada dirección por separado:
1 'selectors' => [ 2 '{{WRAPPER}} .elementor-svg-divider-basic-button' => 3 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};', 4 ],
Imagen de fondo de una diapositiva en un control repetidor:
1 'selectors' => [ 2 '{{WRAPPER}} {{CURRENT_ITEM}} .swiper-slide-bg' => 3 'background-image: url({{URL}})', 4 ],
Posición condicional para RTL. El mismo control proporciona diferentes propiedades según la dirección del texto:
1 'selectors' => [ 2 'body:not(.rtl) {{WRAPPER}} .dialog-close-button' => 'right: {{SIZE}}{{UNIT}}', 3 'body.rtl {{WRAPPER}} .dialog-close-button' => 'left: {{SIZE}}{{UNIT}}', 4 ],
Cómo obtener un valor de otro control
Si dos campos afectan al mismo CSS, no duplique el array, simplemente haga referencia al control vecino:
1 'selectors' => [ 2 '{{WRAPPER}} svg.sde-classic' => 3 'stroke-dasharray: {{dash_length.SIZE}} {{whitespace_length.SIZE}};', 4 ],
Aquí dash_length y whitespace_length son IDs de otros controles en el mismo widget. Sin llamadas adicionales, solo notación de punto.
Versión responsive, los valores se obtienen considerando el dispositivo. Ejemplo real de Elementor Pro:
1 'selectors' => [ 2 '(desktop).elementor-msie {{WRAPPER}} .elementor-portfolio-item' => 3 'width: calc( 100% / {{columns.SIZE}} ); border: {{SIZE}}px solid transparent', 4 '(tablet).elementor-msie {{WRAPPER}} .elementor-portfolio-item' => 5 'width: calc( 100% / {{columns_tablet.SIZE}} ); border: {{SIZE}}px solid transparent', 6 '(mobile).elementor-msie {{WRAPPER}} .elementor-portfolio-item' => 7 'width: calc( 100% / {{columns_mobile.SIZE}} ); border: {{SIZE}}px solid transparent', 8 ],
Cada breakpoint recibe su propio valor de columns. Las propiedades restantes (border, SIZE) son comunes, no están vinculadas al dispositivo.
Selectors_dictionary, switch-case para CSS
La principal capacidad subestimada. selectors_dictionary reemplaza {{VALUE}} con una cadena fija, convirtiendo esencialmente el valor del control en una clave de diccionario.
Tome el control estándar Alinear con opciones izquierda/centro/derecha. Sin un diccionario usted escribiría algo poco natural:
1 'selectors' => [ 2 $sde_selector => 'margin: 0 auto; margin-{{VALUE}}: 0;', 3 ],
Para center esto produce margin: 0 auto; margin-center: 0;. La propiedad margin-center no existe, el navegador la ignora silenciosamente. Pero queda desprolijo.
El diccionario hace lo mismo de forma limpia:
1 'selectors_dictionary' => [ 2 'left' => 'margin-right: auto', 3 'center' => 'margin: 0 auto', 4 'right' => 'margin-left: auto', 5 ], 6 'selectors' => [ 7 '{{WRAPPER}} .sde' => '{{VALUE}}', 8 ],
Valor del control center → {{VALUE}} se convierte en margin: 0 auto. Eso es todo.
Limitación importante: al activar selectors_dictionary usted pierde el {{VALUE}} original. Si el mismo arreglo tiene otro par selector-declaración que necesita el valor original, recibirá la cadena ya sustituida. He aquí un ejemplo problemático:
1 'selectors' => [ 2 '{{WRAPPER}} .sde' => '{{VALUE}}', 3 '{{WRAPPER}}.elementor-sde-scale-the-cropped .sde-cropping-allow .sde' => 4 'transform-origin: {{VALUE}} 0;', 5 ],
Aquí transform-origin recibirá margin: 0 auto 0; en lugar de center 0;. Solución: extraer las declaraciones dependientes a un control separado.
El diccionario también maneja bien la traducción de valores CSS individuales:
1 'selectors_dictionary' => [ 2 'top' => 'flex-start', 3 'middle' => 'center', 4 'bottom' => 'flex-end', 5 ], 6 'selectors' => [ 7 '{{WRAPPER}} .elementor-price-table__currency' => 'align-self: {{VALUE}}', 8 ],
E incluso con conjuntos completos de declaraciones, una clave → múltiples propiedades CSS:
1 'selectors_dictionary' => [ 2 'left' => 'right: auto; left: 0', 3 'right' => 'left: auto; right: 0', 4 ], 5 'selectors' => [ 6 '{{WRAPPER}}.elementor-wc-products ul.products li.product span.onsale' => '{{VALUE}}', 7 ],
Variables CSS, calc() y controles ocultos, armando el rompecabezas
El verdadero poder de los selectores se revela en combinación. Un control establece una variable CSS, otro la referencia, un tercero habilita/deshabilita un bloque entero de reglas mediante una condición.
El deslizador Escala% escribe una variable:
1 'selectors' => [ 2 '{{WRAPPER}} .sde' => '--sde-scale-percentage: {{SIZE}};', 3 ],
La palanca "Escala recortada" usa esta variable en dos lugares, tanto para transform como para pasarla al control Gap:
1 'selectors' => [ 2 '{{WRAPPER}} .sde' => 3 'transform: scale(var(--sde-scale-percentage)) scale(0.01); 4 --sde-scale-pct-for-gap: var(--sde-scale-percentage);', 5 ],
Control oculto con condición, la misma transformación pero con un selector diferente (para el estado no recortado):
1 'condition' => [ 2 'scale_the_cropped!' => 'cropped', 3 ], 4 'selectors' => [ 5 '{{WRAPPER}} .sde svg' => 6 'transform: scale(var(--sde-scale-percentage)) scale(0.01);', 7 ],
Y el control Gap usa la variable recibida con valor de respaldo:
1 'selectors' => [ 2 '{{WRAPPER}} .sde' => 3 'padding: calc({{SIZE}}{{UNIT}} / (var(--sde-scale-pct-for-gap, 100) / 100)) 0;', 4 ],
Lo que ocurre aquí: Gap compensa el escalado. Si un elemento se reduce a la mitad, el espacio se multiplica por 2 para que visualmente permanezca igual. Sin reducción (variable no definida), entra el valor de reserva 100 → división entre 1 → el espacio no cambia. Matemática CSS pura, sin una sola línea de JS.
Apilamiento de transformaciones, solución para Edge
La construcción scale(X) scale(0.01) merece una mención especial. ¿Por qué no scale(calc(var(--sde-scale-percentage) / 100))? Porque Edge no admite calc() dentro de transform. En absoluto.
Solución: apilamiento. Los navegadores aplican las funciones de transformación secuencialmente, una tras otra. Por lo tanto:
1 transform: scale(var(--sde-scale-percentage)) scale(0.01);
Equivale matemáticamente a scale(var(--sde-scale-percentage) * 0.01), es decir, división entre 100. El usuario obtiene un control deslizante familiar de 0 a 100, mientras que internamente el valor se convierte en un coeficiente de 0 a 1.
El mismo principio se aplica a otras transformaciones: rotar, trasladar, sesgar, y funciona en todos los navegadores modernos, incluido Edge.
⁉️🤔 Preguntas frecuentes
¿Qué contiene exactamente el archivo CSS generado?
Elementor recopila todos los selectores de los controles registrados del widget, sustituye los valores actuales de la configuración del usuario y escribe el resultado en
/wp-content/uploads/elementor/css/post-XXXX.css. No son estilos en línea ni CSS dinámico sobre la marcha: es un archivo estático que el navegador almacena en caché y que persiste hasta el siguiente cambio de configuración en el editor. Los propios marcadores de posición{{VALUE}}{{SIZE}}y otros no forman parte de WordPress ni del motor de plantillas Blade: Elementor realiza unstr_replaceconvencional durante la generación del CSS, iterando por todos los pares selector-declaración y reemplazando los tokens con los valores reales de los controles.
¿Cómo depuro los selectores si el CSS no se aplica?
Abra el archivo CSS generado de la entrada (la ruta es visible en el código fuente de la página) y verifique que la regla esté presente. Si la regla falta, busque un error tipográfico en el ID del control o un error de sintaxis en el array
selectors. Si la regla está pero no funciona, revise la especificidad del selector:{{WRAPPER}}proporciona alta prioridad, pero los temas anidados pueden sobrescribir mediante!important. ActiveWP_DEBUGy vigile los registros de PHP: Elementor omite silenciosamente los arrays incorrectos sin mostrar errores en pantalla. Use{{WRAPPER}}SIEMPRE, excepto en casos de segmentación intencionada de body o html.
¿En qué se diferencian los selectores del CSS personalizado en la configuración del widget?
El CSS personalizado (pestaña Avanzado) lo escribe manualmente el usuario, son reglas estáticas que no reaccionan a los cambios de configuración. Los selectores vinculan dinámicamente los controles con el CSS: mueva el deslizador, el
widthcambia; cambie la Alineación, elmarginse reconstruye. El usuario no ve este mecanismo, simplemente obtiene una vista previa en vivo. Para el desarrollador, la principal ventaja es la ausencia del método_content_template(): sin selectores, tendría que escribir el renderizado de la vista previa en JS para cada control.
¿Necesito selectors_dictionary si ya uso selectors?
Sí, para dar un salto cualitativo en la limpieza del código. Sin un diccionario, usted procesa el valor del control implícitamente, mediante propiedades CSS extrañas como el inexistente
margin-center, que el navegador ignora. Con un diccionario, especifica explícitamente: «si el valor es left, sustituya margin-right: auto; si es center, margin: 0 auto». El código se vuelve autodocumentado y, lo más importante,{{VALUE}}ya no arrastra el valor original del control a otras declaraciones del mismo array.
¿Puedo combinar selectors con _content_template() en un mismo widget?
Técnicamente sí, pero en la práctica esto es una señal para reconsiderar la arquitectura. Si los selectores bastan para la mayoría de los controles, pero un par de campos requieren renderizado JS, extraiga la lógica JS a un método separado e invóquelo de forma precisa. Rechazar por completo los selectores en favor de
_content_template()significa que está escribiendo un duplicado en JS de toda la lógica PHP de los controles; mantener un widget así se convierte rápidamente en un problema.
¿Vale la pena dominar los selectores en 2026?
Elementor continúa desarrollando infraestructura atómica, el Gestor de Variables, contenedores Grid y Flexbox, y estilos globales. Pero la base de la mecánica de los widgets no ha cambiado desde la versión cuatro: selectors y selectors_dictionary siguen siendo la forma principal de vincular un control con la vista previa en vivo.
Al dominar esta técnica, usted elimina fácilmente la mitad de toda la lógica JS en un widget típico. En lugar de manejadores para cada campo, un array selectors por control. En lugar de un posicionamiento complejo en la vista previa, una combinación de variables CSS con calc() y un par de controles ocultos. El plugin SVG Divider for Elementor es un ejemplo vivo: más de la mitad de sus controles se gestionan exclusivamente mediante selectores, sin una sola llamada a _content_template().
La regla principal es no complicar en exceso. Si se sorprende a sí mismo escribiendo un cuarto calc() anidado con tres variables, deténgase. Quizás sea más sencillo añadir un control intermediario oculto o dividir la lógica en dos campos separados. Y el código fuente de Elementor es el mejor libro de texto: el método add_control_rules() en core/files/css/base.php muestra cómo se procesan internamente los selectores.



