¿Quién debe encargarse del comportamiento de la interfaz de usuario? Elegir entre HTML, CSS y JavaScript
Un marco para decidir cuándo las funciones nativas de HTML y CSS como dialog, popover, :has() y container queries deben reemplazar a JavaScript, y cuándo no.
Los desarrolladores de frontend recurren instintivamente a JavaScript. Si se necesita abrir un menú, se añade estado; si hay que manejar un clic, se asigna un listener. Si un componente debe adaptarse a su ancho, se mide; si una herramienta de ayuda debe aparecer junto a un botón, se leen las coordenadas; si se necesita un efecto de desplazamiento, se escucha el evento de scroll y se actualizan los estilos. Los frameworks modernos facilitan todo esto: React cuenta con estado y efectos, Angular tiene señales, Vue dispone de refs y reactividad, y Svelte hace que las actualizaciones sean extremadamente concisas. Nada de eso está mal, y JavaScript es una parte importante de por qué el navegador se convirtió en una plataforma de aplicaciones en lugar de un visor de documentos. Sin embargo, HTML y CSS siguen ganando capacidades que se superponen a esta labor. Este artículo explica por qué esto sigue ocurriendo, muestra las primitivas nativas que ahora cubren interacciones comunes y ofrece una regla práctica para decidir qué capa debe encargarse de cada tarea.
de comportamiento.
Por qué la plataforma sigue asumiendo las tareas de las aplicaciones
Si JavaScript ya puede resolver casi todos los problemas de interfaz de usuario, ¿por qué la plataforma sigue creciendo? No porque JavaScript haya fallado. Es porque el navegador va aprendiendo gradualmente a manejar problemas que antes cada aplicación tenía que resolver por sí misma. Cada vez que esto ocurre, los desarrolladores ganan otra opción, y la pregunta más interesante se vuelve cuál opción conlleva el menor costo a largo plazo.
Cada cambio compite por el siguiente frame
Es útil recordar qué hace el navegador después de que algo en la página cambia. Un pipeline de renderizado simplificado se ve así:
JavaScript
↓
Style calculation
↓
Layout
↓
Paint
↓
Composite
Los motores reales son mucho más sofisticados. No todas las actualizaciones pasan por cada etapa; algunas tareas se ejecutan fuera del hilo principal, la composición puede realizarse por separado y ciertas animaciones evitan activar repetidamente el proceso de reorganización o dibujo del contenido. Aun así, una idea de este modelo simplificado sigue siendo válida: el navegador dispone de un tiempo limitado para generar cada frame. La ejecución de scripts, el manejo de entradas, el cálculo de estilos y la renderización deben compartir ese tiempo. Cuanto más trabajo se acumule, más tiempo tardará el navegador en responder. Para conocer con mayor detalle cuánto cuesta cada tipo de cambio, consulte nuestro análisis sobre reflow, repaint y composite.
Es fácil pasar por alto esto en una máquina de desarrollo con un CPU rápido, mucha memoria y una buena conexión. Ejecuta la misma aplicación en un teléfono más antiguo en modo de ahorro de batería con varias aplicaciones abiertas, y los pequeños retrasos se vuelven visibles: una animación pierde un frame, el desplazamiento parece lento y un botón responde con suficiente demora como para parecer defectuoso.
El trabajo de optimización no se trata solo de hacer que el código existente sea más rápido. A menudo, la mejor optimización es no crear tareas que el navegador nunca necesitó que crearas.
Se trata de la responsabilidad, no de HTML contra JavaScript
Sería fácil reducir la discusión a un eslogan:
HTML + CSS = good
JavaScript = bad
Ese eslogan no tiene sentido. JavaScript resuelve problemas para los cuales HTML y CSS nunca fueron diseñados. La edición colaborativa depende de la lógica de la aplicación, y los paneles en tiempo real dependen de mantener los datos sincronizados. Las funciones avanzadas como arrastrar y soltar, reglas de dominio, gráficos interactivos, máquinas de estado y flujos de trabajo de múltiples pasos no desaparecerán solo porque se haya lanzado un nuevo selector.
Los casos que merecen ser reconsiderados son mucho más simples. Supongamos que necesitas mostrar u ocultar alguna información adicional. Un script puede encargarse de eso:
button.addEventListener('click', () => {
panel.hidden = !panel.hidden;
});
Ese es un código perfectamente válido. Sin embargo, HTML puede expresar directamente un widget de divulgación. Tenga en cuenta que el h extra después de la etiqueta de cierre en el fragmento es un error tipográfico y debe eliminarse:
<details>
<summary>Advanced settings</summary>
<p>...</p>
</details>h
La verdadera diferencia no está en el número de líneas. Se trata de quién controla el comportamiento. Con el script, la relación es:
Application owns the behavior
Con <details>, se convierte en:
Browser owns the behavior
Cuando el navegador controla un comportamiento, también se hace cargo de los detalles que de otro modo habría que recordar: el soporte para teclado, el estado expandido disponible para las tecnologías de asistencia y un comportamiento consistente en todas las páginas. Esa distinción es importante con más frecuencia de lo que parece a primera vista.
Por qué HTML no se detuvo en párrafos y formularios
En principio, HTML podría haber dejado de evolucionar hace tiempo. Los títulos, párrafos, formularios, botones, enlaces, imágenes y contenedores genéricos eran suficientes para crear cualquier cosa, dejando todo lo más complejo en manos de los scripts.
Pero considere cómo es ese mundo. Cada aplicación reinventa cada tipo de revelación de información, diálogo, ventana emergente y comportamiento de formulario, de manera ligeramente diferente. Las bibliotecas parecen normalizar esos patrones, los frameworks envuelven esas bibliotecas, y cada usuario descarga otra implementación de algo que el navegador podría entender de forma nativa.
Cuando un patrón aparece en todas partes, una primitiva integrada comienza a valerse por sí misma. Visto de esta manera, el HTML moderno no se está convirtiendo en un framework de aplicaciones; está codificando gradualmente las interacciones que casi todas las aplicaciones siguen reconstruyendo.
Ventanas emergentes sin estado de aplicación
Tomemos un pequeño panel de configuraciones. Una versión de componente suele comenzar con una primitiva de estado, como una señal en este ejemplo o su equivalente en otro framework:
const open = signal(false);
Luego, los requisitos van acumulándose:
button click
↓
change state
↓
show overlay
↓
handle outside click
↓
handle Escape
↓
manage stacking
A veces el producto realmente necesita todo eso. Para casos más simples, HTML ahora ofrece la API Popover. Un botón apunta a un elemento con el atributo popover mediante popovertarget, y el navegador se encarga de activar y desactivarlo:
<button popovertarget="settings">
Settings
</button>
<div id="settings" popover>
...
</div>
Con el comportamiento predeterminado de popover (auto), el navegador también se ocupa de cerrarlo de forma sencilla, al hacer clic fuera de él o presionar Escape, y lo muestra en la capa superior, por lo que no es necesario lidiar con los niveles de z-index. El comportamiento que antes dependía exclusivamente del código de la aplicación ahora cuenta con un punto de partida nativo.
Eso no significa que nunca se deba crear un menú desplegable personalizado. Un sistema de autocompletado con búsqueda remota, virtualización, selección múltiple y manejo personalizado del teclado es un problema completamente distinto. Lo importante es que un simple popover ya no tiene por qué comenzar su existencia como parte del estado de la aplicación.
Diálogos que parten de un elemento real
Los modales son una buena ilustración, ya que parecen sencillos hasta que se construyen adecuadamente. La primera versión consiste en una sola bandera:
open = true;
Y entonces surgen las verdaderas preguntas:
- ¿Qué pasa con el contenido detrás del modal?
- ¿A dónde va el foco cuando se abre?
- ¿Puede cerrarlo el teclado?
- ¿Qué sucede y a dónde vuelve el foco cuando se cierra?
- ¿Se renderiza por encima de todo lo demás, en la capa superior?
- ¿Cómo debería ser y qué función debe tener el fondo de pantalla?
Durante años, muchas implementaciones comenzaban con un <div> genérico y construían todo a mano. Hoy en día, HTML ofrece un elemento dedicado para ello:
<dialog>
...
</dialog>
Un detalle práctico que vale la pena conocer es que el comportamiento modal se obtiene al abrir el elemento con showModal(). Esa llamada coloca el diálogo en la capa superior, hace que el resto de la página quede inactiva, permite cerrarlo con la tecla Escape y proporciona un pseudo-elemento ::backdrop para el estilo. Abrirlo con show() o el atributo open genera, en cambio, un diálogo no modal.
El elemento nativo no resuelve todos los requisitos relacionados con los modales, y las aplicaciones reales pueden seguir añadiendo lógica alrededor de él. Pero el punto de partida es diferente: ya no se tiene que inventar el concepto de diálogo, porque el navegador ya sabe qué es. Esto refleja un patrón en las plataformas modernas: las primitivas nativas no eliminan la lógica de la aplicación; reducen la cantidad de ella que uno debe gestionar.
CSS que reacciona al estado del DOM
HTML no es la única capa que asume tareas. Ahora CSS puede expresar relaciones que antes requerían scripts.
Considere un campo de formulario cuyo contenedor debe estilizarse cuando su valor sea inválido. Un script podría inspeccionar el campo y mantener una clase en el elemento padre sincronizada. El CSS moderno a menudo puede expresar directamente esa relación:
.field:has(input:invalid) {
/* invalid state */
}
:has() modifica lo que CSS puede conocer sobre el documento. Se solicitó durante mucho tiempo como un selector padre faltante, pero la consecuencia arquitectónica es más importante que el nombre: algunas sincronizaciones de estado ya no necesitan pasar por tu JavaScript en absoluto. El DOM ya contiene la información, y CSS puede responder a ella. No hay clases que olvidar actualizar ni listeners que limpiar.
Componentes que responden a su propio espacio
El diseño responsive se ha basado tradicionalmente en consultas de medios:
@media (max-width: 700px) {
...
}
Eso funciona bien cuando lo importante es el área de visualización. Sin embargo, los componentes a menudo se preocupan por otra cosa. Imagina la misma tarjeta renderizada en dos lugares:
Main content
┌───────────────────────────────┐
│ Card │
└───────────────────────────────┘
Sidebar
┌─────────────┐
│ Card │
└─────────────┘
La vista en pantalla es idéntica en ambos casos; el espacio disponible para la tarjeta no lo es. Los sistemas de componentes reutilizables solían abordar esto con código de medición: observar el elemento, leer su ancho, agregar una clase y repetirlo cada vez que cambia el diseño.
Las consultas de contenedor permiten que CSS responda a la pregunta real, es decir, cuánto espacio tiene realmente este componente. Se marca un ancestro como contenedor y se escriben reglas relacionadas con su tamaño:
.card-container {
container-type: inline-size;
}
@container (width < 400px) {
.card {
/* compact layout */
}
}
Esto es más que una nueva sintaxis responsive; traslada la responsabilidad. El navegador ya ha calculado el diseño, y CSS puede utilizar ese resultado sin que el código de la aplicación lo mida nuevamente. Nuestra comparación entre consultas de contenedor y consultas multimedia analiza cuándo cada una encaja en un sistema de diseño.
El posicionamiento muestra cómo crece la plataforma
Añadir una herramienta de ayuda a un botón parece una tarea sencilla hasta que realmente la intentas. ¿Dónde está exactamente el botón? ¿Qué tamaño tiene la herramienta de ayuda? ¿Hay espacio abajo o debería aparecer arriba? ¿Qué sucede al desplazarse, al cambiar el tamaño de la ventana de visualización o cuando otra parte del diseño mueve el botón? Una simple herramienta de ayuda se convierte rápidamente en un problema geométrico.
Por eso existen bibliotecas especializadas en interfaces flotantes, las cuales resuelven muy bien un problema realmente complejo. CSS ahora cuenta con el posicionamiento por anclaje, que permite posicionar un elemento de forma relativa a otro elemento de manera declarativa. En el momento de redactar este texto, la compatibilidad con los navegadores aún era desigual, así que verifica los datos de compatibilidad actuales y ten una solución alternativa si decides adoptarlo.
Lo que más importa no es si CSS reemplaza de la noche a la mañana todas las bibliotecas de posicionamiento. Se trata de que la plataforma reconozca el posicionamiento como algo lo suficientemente común como para merecer primitivas de nivel más bajo. Ese es el ciclo habitual de crecimiento de la plataforma: los problemas surgen en las aplicaciones, se repiten los patrones, aparecen las bibliotecas y, finalmente, la plataforma aprende de ellas.
Animación y el valor de las APIs declarativas
La animación es donde la discusión sobre el rendimiento adquiere matices. Existe un dicho popular que afirma que las animaciones en CSS se ejecutan en la GPU mientras que las animaciones en JavaScript lo hacen en la CPU. La realidad es más compleja: las animaciones en CSS siguen requiriendo recursos, JavaScript puede generar animaciones muy fluidas, y las propiedades de CSS difieren enormemente en cuanto a su costo computacional. Animar dimensiones puede provocar cambios en el diseño, otras propiedades necesitan una nueva pintura de la pantalla, mientras que transform y opacity suelen ser mucho más amigables para el compositor.
Por lo tanto, la distinción útil no es entre CSS y JavaScript, sino cuánto trabajo implica cada cambio y qué libertad se le da al navegador para programar ese trabajo.
Las animaciones controladas por el desplazamiento ilustran este cambio. Un efecto de desplazamiento solía funcionar mediante este bucle:
scroll event
↓
JavaScript calculation
↓
update styles
↓
repeat
Algunas de estas relaciones ahora pueden declararse directamente al motor de renderizado. En lugar de indicar repetidamente al navegador que calcule un valor, lo aplique y vuelva a hacerlo, se describe cómo una animación avanza a lo largo de una línea de tiempo de desplazamiento. Se trata de una categoría completamente diferente de API, en la cual el navegador controla más del mecanismo y puede optimizarlo. Al igual que con la posición de los anclajes, verifique el soporte en los navegadores a los que se dirige.
Eliminar JavaScript elimina más que solo bytes
La reducción de JavaScript suele discutirse en kilobytes, y eso es importante, porque cada script debe pasar por este proceso antes de poder hacer algo útil:
downloaded
↓
parsed
↓
compiled
↓
executed
Pero el ciclo de vida del código de la aplicación continúa mucho tiempo después de que se ejecute por primera vez. El comportamiento implementado en JavaScript tiende a traer consigo:
state
event listeners
synchronization
cleanup
framework lifecycle
hydration
tests
Ahora imagine que una primitiva nativa le permite eliminar esta combinación:
state
+
outside-click listener
+
Escape handler
+
DOM synchronization
El código eliminado podría pesar solo unas pocascientas bytes, pero el cambio arquitectónico es real. Su aplicación ya no mantiene ese comportamiento. Hay una variable de estado menos que puede volverse obsoleta, un listener menos que alguien podría olvidarse de eliminar, una relación DOM menos que debe mantenerse sincronizada con el estado, y una interacción menos que hay que reconstruir desde cero, a menudo con una accesibilidad peor que la versión nativa. Con frecuencia, el mayor beneficio de una función de la plataforma no es la velocidad, sino la responsabilidad que se devuelve al navegador.
La plataforma no tiene versión final
Un concepto erróneo persistente ayuda a explicar por qué los desarrolladores pasan por alto estos cambios: hablar de “HTML5” como si HTML hubiera alcanzado la versión cinco y luego se hubiera detenido prácticamente. Eso no ocurrió. No existe un HTML6 en desarrollo ni un HTML7 al que esperar. HTML se mantiene como un estándar en evolución, y la especificación progresa continuamente.
CSS sigue un modelo similar. Muchos desarrolladores aprendieron “CSS3”, pero CSS ya no se entrega como un único paquete grande con versiones definidas. Los módulos separados avanzan a su propio ritmo, y por eso se encuentran nombres como estos sin que exista un “CSS6” general que los englobe:
Selectors Level 4
CSS Color Level 5
CSS Cascade Level
La última entrada está truncada en el fragmento; el módulo de Anidamiento e Herencia también cuenta con su propio número de nivel. Dado que los módulos son independientes, la cuadrícula puede evolucionar sin esperar a los selectores, el trabajo con colores se realiza por separado de las fuentes, y las consultas a contenedores se implementaron sin esperar ningún hito imaginario de CSS4. A escala de la web, la evolución incremental es el único enfoque sensato.
JavaScript también sigue avanzando
Lo mismo ocurre con el lenguaje. La especificación estandarizada es ECMAScript; JavaScript es el nombre que utilizan los desarrolladores para referirse al lenguaje que los motores implementan según dicha especificación. Por eso el progreso se describe en ediciones de ECMAScript en lugar de versiones numeradas de “JavaScript”, y siguen apareciendo nuevas capacidades.
Así que la plataforma no está congelada gracias a esta combinación:
HTML5
+
CSS3
+
JavaScript
Todos los tres niveles siguen evolucionando. ECMAScript incorpora primitivas más avanzadas, CSS asume más responsabilidades en cuanto a diseño, estilos e interacciones, y HTML adquiere semántica y comportamientos que antes las aplicaciones tenían que implementar por sí mismas. Con el tiempo, los límites entre ellos cambian, y por eso lo que aprendiste sobre la plataforma hace años necesita actualizaciones periódicas.
El conocimiento de la plataforma trasciende las elecciones de framework
Estas funcionalidades existen independientemente de cualquier framework. Una consulta de contenedor funciona de la misma manera ya sea que el componente haya sido creado con Angular, React, Vue, Svelte o código DOM simple. :has() no se preocupa por cómo se generó el DOM. Un <dialog> nativo sigue siendo un diálogo dentro de un framework, y un popover es una función del navegador sin importar qué lo haya renderizado.
Eso hace que el conocimiento de las plataformas sea excepcionalmente duradero. La experiencia con un framework se transfiere a los proyectos que utilizan ese mismo framework; la experiencia con una plataforma se transfiere a casi todos los proyectos web. Esto no disminuye la importancia de los frameworks, ya que resuelven problemas arquitectónicos importantes y relacionados con la experiencia del desarrollador. Comprender la capa subyacente les brinda una opción adicional cuando una abstracción resulta innecesaria y, lo que es igualmente importante, le muestra qué está haciendo esa abstracción por usted. Si desea un catálogo de sustituciones específicas, nuestro resumen sobre las características nativas de HTML que reemplazan a las bibliotecas UI comunes lo aborda característica por característica.
Cuando lo nativo no es la opción adecuada
Antes de dejarse llevar, una advertencia: poder crear algo con HTML o CSS no significa que deba hacerse. Considere un menú desplegable que requiere:
- búsqueda asíncrona;
- cientos de opciones virtualizadas;
- selección múltiple;
- navegación por teclado avanzada;
- reglas de filtrado personalizadas;
- opciones cargadas desde el servidor;
- selecciones sincronizadas con el estado de la aplicación.
La existencia de un simple popover no resuelve ese problema en absoluto, y deformar el diseño para evitar JavaScript empeoraría la arquitectura en lugar de mejorarla.
CSS merece el mismo realismo. Realiza tareas reales: el diseño y la renderización no son gratuitos, un DOM enorme sigue siendo costoso, y las animaciones mal elegidas pueden seguir teniendo un rendimiento deficiente.
Por lo tanto, el objetivo no es utilizar HTML y CSS siempre que sea técnicamente posible. Una regla mucho más útil es esta:
Utilice la capa más básica que resuelva de manera adecuada el requisito real.
A veces esa capa es HTML, otras veces CSS, otras veces JavaScript, y la mayoría de las aplicaciones sólidas combinan los tres. Una forma rápida de aplicar esta regla es hacerse la siguiente pregunta en orden: ¿un elemento o atributo semántico expresa este comportamiento? Si no, ¿puede CSS derivarlo del estado que ya tiene el DOM? Solo cuando ambas respuestas sean negativas, o cuando el requisito supere las capacidades de la versión nativa, debería ese comportamiento trasladarse al código de la aplicación.
Los casos más simples merecen las soluciones más sencillas
Las funciones complejas seguirán requiriendo JavaScript, y eso está bien. Pero alrededor de esas funciones, la mayoría de las aplicaciones contienen cientos de interacciones menores: secciones expandibles, superposiciones ligeras, tarjetas que se adaptan a su posición, estilo para campos inválidos, transiciones simples, un elemento que sigue a otro. Cada vez que un elemento nativo, un atributo o una regla CSS breve cubre alguna de ellas, es posible que su aplicación no necesite manejarla en absoluto.
El hecho de que JavaScript pueda resolver el problema no es algo notable; puede solucionar casi cualquier cosa. Lo valioso es contar con otra opción.
Actualizando el modelo mental
Muchos desarrolladores aprendieron sobre la web a través de una división clara del trabajo:
HTML → structure
CSS → presentation
JavaScript → behavior
Es un buen modelo para aprender, pero los navegadores actuales son más capaces de lo que sugiere. HTML ahora incluye comportamientos, CSS responde a relaciones complejas en el DOM y en la maquetación, y el propio lenguaje sigue ganando funcionalidades. La plataforma seguía cambiando incluso cuando los hábitos permanecían inmutables.
Muchos de esos hábitos se consideraban hechos indiscutibles: las superposiciones necesitan estado de la aplicación, el posicionamiento requiere scripts, los componentes solo pueden responder a puntos de ruptura del viewport y las animaciones de desplazamiento necesitan listeners. Quizás todo eso fuera cierto en algún momento; algunos ya no lo son siempre.
Por eso, mantenerse al día con HTML y CSS no se trata de acumular sintaxis ingeniosa. Una nueva capacidad del navegador puede eliminar una dependencia, un listener de eventos, un fragmento de estado, un ResizeObserver, o cualquier código cuya única función era reproducir algo que ahora el navegador entiende de forma nativa.
Puntos clave
- El navegador sigue adoptando comportamientos que las aplicaciones implementan repetidamente; cada adopción ofrece una opción de menor costo.
- El tiempo de renderizado es un presupuesto compartido, por lo que evitar trabajos innecesarios suele ser la optimización más efectiva.
- Piense en términos de responsabilidad:
<details>, la API Popover y<dialog>conshowModal()permiten que el navegador se encargue del comportamiento, incluidos los detalles de accesibilidad que de otro modo tendría que mantener usted. :has()y las consultas de contenedor permiten que el CSS reaccione al estado del DOM y al tamaño de los componentes sin necesidad de código de sincronización o medición.- El posicionamiento de anclajes y las animaciones controladas por el desplazamiento apuntan en la misma dirección, pero verifique la compatibilidad actual del navegador antes de depender de ellos.
- Lo nativo no es automáticamente mejor: los widgets complejos siguen correspondiendo a JavaScript, y el trabajo con CSS también tiene costos reales.
Lecturas relacionadas
- Estructura, estilo y comportamiento: cómo HTML, CSS y JavaScript dividen el trabajo — Un mapa fácil de entender para principiantes que muestra qué funciones tiene cada uno de HTML, CSS y JavaScript en una página web, cómo colaboran entre sí y qué errores comunes difuminan las líneas que los separan.
- API nativas del navegador que sustituirán a paquetes populares de npm en 2026 — Explica cómo las características nativas de JavaScript y CSS, como Signals, el operador pipeline, Temporal y el posicionamiento por anclaje, están reemplazando a paquetes comunes de npm.
- Un botón de retroalimentación con gesto de pulgar para arriba animado sin ninguna biblioteca de animaciones — Crea un widget de retroalimentación con gesto de pulgar para arriba y para abajo que incluya una animación de aparecimiento con keyframes, estados activos y una llamada a fetch, utilizando únicamente HTML, CSS y unas pocas líneas de JavaScript.
- Typed CSS attr(): Introducir valores de atributos HTML directamente en los estilos — Aprende cómo typed CSS attr() convierte atributos de datos en longitudes, colores y tiempos, cómo fallan sus soluciones alternativas, por qué url() está bloqueado y cómo implementarlo de forma segura hoy en día.