Sistemas

El botón que miente

6 min

Un botón sin estado loading no está terminado. Está en producción con un bug pendiente.

Envías un formulario. La página no responde. Haces click otra vez.

Acabas de crear dos requests idénticas.

No es un bug del servidor. Es una decisión que nadie tomó.

El estado que siempre falta

Un componente button tiene seis estados mínimos: default, hover, active, focus, disabled, loading.

La mayoría de los sistemas de diseño documentan cuatro. Los dos que faltan son los que más importan en producción.

El estado loading ocurre en el momento más crítico de la interacción: justo después de que el usuario tomó una decisión. Es el único estado que el usuario no puede provocar de nuevo fácilmente si se pierde. Y es el único que protege al servidor de recibir la misma petición tres veces porque la interfaz no comunicó que algo estaba ocurriendo.

En Vocento, cuando construí EVOLOK — el sistema de diseño para 13 marcas distintas — uno de los problemas estructurales era exactamente este. Los componentes de checkout estaban construidos de forma diferente en cada equipo. Sin estados compartidos, sin tokens de loading, sin un contrato claro entre diseño y código sobre qué hace el botón después del primer click. El A/B testing era estadísticamente inválido porque cada variante se comportaba diferente. No en visual — en comportamiento.

Un sistema de diseño que no especifica el estado loading de sus botones no tiene un sistema de diseño. Tiene una librería de estilos.

Y esto no es un problema de checkout de medios. Es el mismo bug en el botón «Guardar» de un dashboard interno, en el «Crear proyecto» de una app de gestión, en el «Confirmar pago» de un checkout B2B, en el botón de submit de cualquier formulario que dispare una request de red. El componente es el mismo. El bug es el mismo. Solo cambia el contexto de negocio.

Por qué ocurre

En la mayoría de los proyectos el estado loading no se dibuja en Figma porque «lo gestiona el backend». El developer lo implementa como puede — o no lo implementa. El resultado llega a producción sin que nadie lo haya revisado porque nadie lo especificó.

El flag de JavaScript que controla si el botón está procesando es trivial de implementar. Lo que no es trivial es que el design system lo exija:

// Sin control — el botón acepta clicks ilimitados
button.addEventListener('click', async () => {
  await submitForm();
});

// Con flag — desactiva inmediatamente, restaura al terminar
let isLoading = false;

button.addEventListener('click', async () => {
  if (isLoading) return;

  isLoading = true;
  button.disabled = true;
  button.setAttribute('data-state', 'loading');

  try {
    await submitForm();
    button.setAttribute('data-state', 'success');
  } catch {
    button.setAttribute('data-state', 'error');
  } finally {
    isLoading = false;
    button.disabled = false;
    button.setAttribute('data-state', 'default');
  }
});
La diferencia no está en el try/catch. Está en la línea if (isLoading) return.

La diferencia no está en el try/catch. Está en la línea if (isLoading) return. Sin esa línea, el botón acepta todos los clicks que lleguen mientras la request está en vuelo. Da igual si esa request crea un pedido, guarda un registro, o dispara un pago — el bug es idéntico.

El gap entre Figma y producción

El problema no es que nadie documente el estado loading. Es que la documentación depende de que alguien recuerde hacerlo.

En Vocento construí un pipeline que eliminó esa dependencia.

El flujo era simple en concepto y difícil de mantener sin automatización: diseño en Figma con autolayout preciso → Cursor con una skill específica según el tipo de componente que se estaba maquetando → el agente construye y documenta simultáneamente → archivos MD con la spec completa → Confluence como fuente de verdad para equipos que no coordinaban entre sí directamente.

El punto crítico es que el agente documentaba todo lo que maquetaba. No como paso opcional — como parte del mismo proceso. Si el componente tenía un estado loading, ese estado aparecía en el MD. Si no lo tenía, el MD lo dejaba en evidencia porque la spec estaba incompleta.

Los developers no recibían un Figma link y una llamada de explicación. Recibían una implementación de referencia en código y la documentación de cada decisión tomada durante la construcción.

El estado loading dejó de ser algo que alguien tenía que recordar especificar. Pasó a ser algo que el sistema hacía imposible omitir.

Eso es la diferencia entre un design system y diseño de infraestructura. Y es la misma diferencia entre un botón que funciona en la demo y un botón que sobrevive a producción a escala — sea el checkout de un ecommerce, el formulario de onboarding de un SaaS, o el botón de deploy de un dashboard interno.

La regla

Cualquier acción que dispare una request de red desactiva su trigger inmediatamente.

No después de que llegue la respuesta. Inmediatamente.

El tiempo entre click y desactivación es el tiempo en que la interfaz miente sobre su estado.

El problema, y la solución

Mismo botón, misma request de 2 segundos. A la izquierda no hay control de estado: cada click añade una request. A la derecha, una línea — if (isLoading) return — lo cambia todo. Pruébalos.

Los seis estados

Fíjalo en cada estado y míralo con calma. Focus también se activa con un Tab real del teclado.

El pipeline

De Figma a Confluence. Recorre cada paso para ver dónde se documenta cada decisión.

Antes y después del handoff

El mismo trabajo, dos formas de entregarlo. Simula cada columna y observa dónde aparece — o desaparece — el estado loading.

La spec del componente

Esto es lo que recibe un developer cuando el sistema funciona: la documentación completa, con el estado loading ya dentro.