Escribir JavaScript interactivo sin frameworks en 2026

Web Components, diálogos nativos, popovers, View Transitions y estado reactivo con EventTarget. Un recorrido práctico por las APIs del navegador que permiten construir interfaces dinámicas sin React, sin Vue y sin engordar el node_modules.

Mesa de taller con un reloj mecánico abierto y reluciente, montado solo con unas pocas piezas precisas, junto a una caja rebosante de piezas sobrantes que nadie ha necesitado

Decir en voz alta que una interfaz interactiva se puede construir sin framework sigue provocando, en según qué foros, la misma reacción que proponer ir de Bilbao a Madrid en diligencia. Se da por hecho que cualquier cosa que se mueva en pantalla, que abra una ventana, que cambie un contador o que filtre una lista necesita React, Vue o alguno de sus primos. Y sin embargo, en 2026, esa suposición tiene cada vez menos fundamento.

En El día que olvidamos cómo funciona el navegador la reflexión iba sobre el exceso de capas. Este artículo es la parte práctica: qué ofrece hoy el navegador de serie, cómo se usa y hasta dónde se puede llegar con JavaScript nativo, HTML bien escrito y un poco de CSS. La respuesta corta es que bastante más lejos de lo que la mayoría imagina.

Por qué ahora sí es posible

Durante años, los frameworks fueron una respuesta razonable a problemas reales. Los navegadores se comportaban de forma distinta entre sí, el DOM era torpe de manipular, no existían módulos nativos y cualquier componente mínimamente complejo exigía reinventar la rueda. Tenía sentido delegar todo eso en una librería que lo resolviera por nosotros.

Ese mundo ya no existe. Los navegadores actuales comparten motor o, como mínimo, estándares; el JavaScript moderno tiene clases, módulos, campos privados y async/await; y el propio HTML ha ido absorbiendo patrones de interfaz que antes había que programar a mano. Ventanas modales, menús emergentes, acordeones, transiciones animadas entre estados y componentes reutilizables encapsulados son hoy funcionalidades nativas. Lo que falta, en muchos casos, no es tecnología sino costumbre.

Conviene aclarar que la idea no es abolir los frameworks, sino dejar de usarlos por defecto. Para una web de contenidos, una tienda pequeña, un panel sencillo o una landing con algo de interacción, el navegador ya trae casi todo lo necesario, sin compilación, sin dependencias y sin cientos de kilobytes que el usuario tenga que descargar.

Diálogos nativos: adiós a las modales artesanales

Pocas cosas se han programado tantas veces de forma defectuosa como una ventana modal. Capa oscura de fondo, bloqueo del scroll, gestión del foco, cierre con la tecla Escape, accesibilidad para lectores de pantalla… Cada librería lo resolvía a su manera y muchas lo resolvían mal. El elemento <dialog> hace todo eso de serie.

<button type="button" id="abrir-detalles">Ver detalles</button>

<dialog id="detalles">
  <h2>Detalles del pedido</h2>
  <p>Tu pedido saldrá del almacén mañana por la mañana.</p>
  <form method="dialog">
    <button>Cerrar</button>
  </form>
</dialog>

<script>
  const dialogo = document.getElementById('detalles');
  document.getElementById('abrir-detalles')
    .addEventListener('click', () => dialogo.showModal());
</script>

Con showModal() el navegador coloca el diálogo en la capa superior, vuelve inerte el resto de la página, atrapa el foco dentro de la ventana y permite cerrarla con Escape. El formulario con method="dialog" cierra la ventana sin recargar nada, y el valor del botón pulsado queda disponible en la propiedad returnValue, lo que resulta muy práctico para confirmaciones del tipo aceptar o cancelar. El fondo oscuro se estiliza con el pseudoelemento ::backdrop, sin añadir un solo div extra.

Los navegadores más recientes han ido incorporando además los comandos declarativos, que permiten abrir y cerrar diálogos sin escribir JavaScript, con atributos como commandfor="detalles" y command="show-modal" en el propio botón. Su soporte es ya amplio, pero merece la pena comprobarlo en caniuse antes de prescindir del pequeño script de respaldo.

Popovers: menús y tooltips con dos atributos

Si el diálogo cubre las ventanas modales, la API de popover cubre todo lo demás que flota sobre la página: menús desplegables, avisos, selectores, paneles de ayuda. Y lo hace con una sencillez que casi da vergüenza.

<button popovertarget="menu-usuario">Mi cuenta</button>

<div id="menu-usuario" popover>
  <a href="/perfil/">Perfil</a>
  <a href="/pedidos/">Mis pedidos</a>
  <a href="/salir/">Cerrar sesión</a>
</div>

Sin una sola línea de JavaScript, el botón abre y cierra el menú, este aparece por encima de todo lo demás, se cierra al pulsar fuera o al presionar Escape, y el navegador gestiona las relaciones de accesibilidad entre el botón y el panel. Si en algún momento se necesita control programático, existen los métodos showPopover(), hidePopover() y togglePopover(), además del evento toggle para reaccionar a los cambios.

Combinado con el posicionamiento por anclaje de CSS, que permite pegar el popover a su botón sin calcular coordenadas a mano, se cubre un porcentaje enorme de las necesidades que antes justificaban librerías como Popper o Floating UI. En este caso también conviene revisar el soporte concreto, porque es de las incorporaciones más recientes.

Acordeones exclusivos sin programar nada

El elemento <details> lleva años entre nosotros, pero ha ganado un superpoder discreto: el atributo name. Cuando varios <details> comparten el mismo nombre, se comportan como un grupo en el que solo uno puede estar abierto a la vez. Es decir, un acordeón clásico sin JavaScript.

<details name="faq" open>
  <summary>¿Hacéis envíos internacionales?</summary>
  <p>Sí, a toda la Unión Europea.</p>
</details>

<details name="faq">
  <summary>¿Cuánto tarda un pedido?</summary>
  <p>Entre 24 y 72 horas laborables.</p>
</details>

Es un ejemplo pequeño, pero ilustra muy bien la filosofía de todo el artículo: antes de escribir código, merece la pena preguntarse si el HTML ya lo hace. Cada vez con más frecuencia, la respuesta es que sí, y además con mejor accesibilidad que la que tendría una solución hecha a mano.

Web Components: componentes de verdad, sin compilar

Aquí llega la pieza gorda. Los Web Components son el conjunto de estándares que permite crear etiquetas HTML propias, con su comportamiento, su estructura interna y sus estilos encapsulados. Son, literalmente, componentes reutilizables al estilo de los frameworks, pero entendidos directamente por el navegador y compatibles con cualquier otra tecnología de la página.

Un ejemplo razonablemente completo: un botón de «me gusta» con contador, estado activo, estilos propios y un evento personalizado para avisar al resto de la página.

class BotonMeGusta extends HTMLElement {
  static observedAttributes = ['total'];
  #activo = false;

  constructor() {
    super();
    this.attachShadow({ mode: 'open' }).innerHTML = `
      <style>
        button {
          font: inherit;
          border: 1px solid currentColor;
          border-radius: 2em;
          padding: .4em 1em;
          background: none;
          cursor: pointer;
        }
        :host([activo]) button {
          background: crimson;
          color: white;
        }
      </style>
      <button type="button" aria-pressed="false">♥ <span></span></button>
    `;
    this.boton = this.shadowRoot.querySelector('button');
    this.contador = this.shadowRoot.querySelector('span');
    this.boton.addEventListener('click', () => this.alternar());
  }

  get total() {
    return Number(this.getAttribute('total')) || 0;
  }

  connectedCallback() {
    this.contador.textContent = this.total;
  }

  attributeChangedCallback() {
    this.contador.textContent = this.total;
  }

  alternar() {
    this.#activo = !this.#activo;
    this.toggleAttribute('activo', this.#activo);
    this.boton.setAttribute('aria-pressed', String(this.#activo));
    this.setAttribute('total', this.total + (this.#activo ? 1 : -1));
    this.dispatchEvent(new CustomEvent('megusta', {
      detail: { activo: this.#activo },
      bubbles: true,
      composed: true
    }));
  }
}

customElements.define('boton-me-gusta', BotonMeGusta);

Y su uso en cualquier página es tan simple como escribir una etiqueta:

<boton-me-gusta total="42"></boton-me-gusta>

Merece la pena detenerse en lo que está pasando aquí. El Shadow DOM encapsula los estilos, de modo que el CSS del componente no se filtra al resto de la página ni el de la página rompe el componente. Los atributos observados funcionan como las props de cualquier framework: si alguien cambia total desde fuera, el contador se actualiza solo. El selector :host([activo]) permite que el componente cambie de aspecto según su propio estado. Y el evento personalizado, al atravesar el Shadow DOM gracias a composed: true, permite que cualquier parte de la aplicación escuche lo que ocurre dentro del componente.

Todo esto sin compilación, sin JSX y sin DOM virtual. El archivo se carga tal cual, el navegador lo entiende tal cual y cualquiera que abra el código fuente puede leerlo tal cual.

Mejora progresiva: el componente que funciona antes de cargar

Uno de los errores más comunes al adoptar Web Components es trasladar el vicio de los frameworks y dejar la página vacía hasta que el JavaScript se ejecuta. Es mucho más interesante usarlos para mejorar HTML que ya funciona. Un componente puede envolver contenido normal y, al cargarse, añadirle comportamiento.

<lista-filtrable>
  <input type="search" placeholder="Filtrar destinos…">
  <ul>
    <li>Oporto</li>
    <li>Liubliana</li>
    <li>Tallin</li>
    <li>Skopie</li>
  </ul>
</lista-filtrable>

<script type="module">
  customElements.define('lista-filtrable', class extends HTMLElement {
    connectedCallback() {
      const campo = this.querySelector('input');
      const elementos = [...this.querySelectorAll('li')];
      campo.addEventListener('input', () => {
        const texto = campo.value.toLowerCase();
        elementos.forEach(li => {
          li.hidden = !li.textContent.toLowerCase().includes(texto);
        });
      });
    }
  });
</script>

Si el script tarda, falla o el usuario lo tiene desactivado, la lista sigue ahí, completa y legible. Los buscadores la indexan sin necesidad de ejecutar nada. Y cuando el JavaScript llega, simplemente añade la capacidad de filtrar. Este enfoque, que muchos llaman componentes HTML en contraposición a los componentes renderizados por completo en JavaScript, es probablemente la forma más sensata de trabajar para webs de contenido, y resulta especialmente amable con el SEO.

Estado reactivo sin librerías

La gran pregunta que suele aparecer en este punto es cómo gestionar el estado compartido. En un framework, varios componentes pueden reaccionar al mismo dato; ¿cómo se hace eso sin él? La respuesta está en una clase que el navegador trae de serie y que casi nadie aprovecha: EventTarget.

// almacen.js
class Almacen extends EventTarget {
  #estado;

  constructor(inicial) {
    super();
    this.#estado = inicial;
  }

  get estado() {
    return this.#estado;
  }

  actualizar(cambios) {
    this.#estado = { ...this.#estado, ...cambios };
    this.dispatchEvent(new Event('cambio'));
  }
}

export const carrito = new Almacen({ productos: [] });

Cualquier componente puede suscribirse a los cambios y cualquier otro puede provocarlos:

import { carrito } from './almacen.js';

carrito.addEventListener('cambio', () => {
  document.querySelector('#contador-carrito').textContent =
    carrito.estado.productos.length;
});

carrito.actualizar({
  productos: [...carrito.estado.productos, { id: 7, nombre: 'Libro' }]
});

Son veinte líneas y cubren el patrón básico de cualquier gestor de estado: una fuente única de verdad, cambios inmutables y notificación a quien le interese. Para aplicaciones más ambiciosas se puede sofisticar con un Proxy que detecte mutaciones automáticamente, o con eventos distintos por cada parte del estado. Existe incluso una propuesta en marcha para incorporar señales reactivas al propio lenguaje, aunque de momento no está disponible de forma nativa y no conviene contar con ella.

Transiciones animadas entre estados

Uno de los argumentos clásicos a favor de los frameworks era la facilidad para animar cambios de vista. La API de View Transitions ha convertido ese argumento en historia. Basta con envolver la actualización del DOM en una llamada y el navegador se encarga de interpolar entre el antes y el después.

function cambiarVista(actualizar) {
  if (!document.startViewTransition) {
    actualizar();
    return;
  }
  document.startViewTransition(actualizar);
}

cambiarVista(() => {
  document.querySelector('main').innerHTML = nuevoContenido;
});

Por defecto se obtiene un fundido suave, pero con CSS se puede personalizar todo: asignando un view-transition-name a un elemento, el navegador lo anima de forma independiente entre su posición antigua y la nueva, lo que permite efectos como una miniatura que crece hasta convertirse en la imagen principal de una ficha. La comprobación inicial garantiza que los navegadores sin soporte simplemente hagan el cambio sin animación, que es exactamente lo que debe hacer una buena mejora progresiva.

Módulos e import maps: organizar el código sin empaquetador

Queda un último fantasma: la organización del código. Sin un empaquetador, ¿cómo se reparte un proyecto en varios archivos? Con módulos nativos, que llevan años funcionando en todos los navegadores. Cada archivo declara lo que exporta, cada archivo importa lo que necesita, y el navegador resuelve las dependencias por su cuenta.

Para evitar rutas relativas interminables existen los import maps, que permiten asignar nombres cortos a los módulos directamente en el HTML:

<script type="importmap">
{
  "imports": {
    "almacen": "/js/almacen.js",
    "componentes/": "/js/componentes/"
  }
}
</script>

<script type="module">
  import { carrito } from 'almacen';
  import './componentes/boton-me-gusta.js';
</script>

Con HTTP/2 y HTTP/3, cargar una docena de módulos pequeños no supone el drama de rendimiento que suponía hace diez años. Para proyectos grandes quizá siga compensando empaquetar en producción, pero para la mayoría de webs el código puede servirse tal cual se escribe. Y eso tiene un valor enorme: lo que se depura en el navegador es exactamente lo que se ha escrito en el editor.

Cuándo sí tiene sentido un framework

Sería poco honesto terminar sin reconocer los límites. Una aplicación con decenas de pantallas, formularios muy complejos, estado compartido enrevesado y un equipo grande que necesita convenciones comunes puede beneficiarse de un framework. También lo hará un proyecto que dependa de un ecosistema concreto de componentes ya hechos, o un equipo cuya experiencia esté toda en una herramienta determinada. Elegir la tecnología también es elegir con quién vas a mantenerla.

Lo que ya no se sostiene es la idea de que cualquier interactividad exige un framework. Un buscador de destinos, una galería con filtros, un carrito de compra sencillo, un formulario con validación, un panel de administración pequeño o una web con modales y menús se construyen hoy con APIs nativas de forma limpia, rápida y mantenible. Sin npm install, sin carpeta node_modules y sin miedo a que dentro de tres años una actualización obligue a reescribirlo todo.

El estándar como apuesta a largo plazo

Hay un argumento que suele pasar desapercibido y que quizá sea el más importante de todos: la durabilidad. Un componente escrito con las APIs estándar del navegador funcionará dentro de diez años, porque la web tiene una obsesión casi religiosa por no romper lo que ya existe. Un proyecto atado a una versión concreta de un framework, en cambio, tiene fecha de caducidad, y esa fecha suele llegar antes de lo que uno espera.

Escribir JavaScript interactivo sin frameworks en 2026 no es un ejercicio de purismo ni una pose retro. Es aprovechar un navegador que ha madurado muchísimo, que ofrece de serie herramientas que antes costaban cientos de kilobytes y que premia a quien lo conoce bien. Basta con sentarse, abrir un archivo HTML en blanco y comprobar lo lejos que se puede llegar sin instalar absolutamente nada.

···
Otras entradas