DEV Community

Silviu Technology
Silviu Technology

Posted on

React: foco y errores accesibles en formularios

Un formulario puede tener colores bonitos, validacion correcta y aun asi ser dificil de usar. El problema suele aparecer cuando alguien pulsa "Continuar" y la interfaz pinta tres errores, mueve el layout y deja el foco en un boton que ya no explica nada. Para quien usa teclado o lector de pantalla, el flujo se vuelve una pequeña busqueda del tesoro.

En mi experiencia con interfaces de signup, el detalle que mas mejora la sensacion de calidad no es otro componente visual. Es un contrato claro: despues de un error, la persona debe saber que paso, donde esta el problema y cual es la siguiente accion. React ayuda a modelar ese estado, pero no lo resuelve por nosotros.

Tambien conviene pensar el onboarding como una secuencia de estados, no como una lista de inputs aislados. Un campo de email, una contraseña y un codigo de verificacion tienen momentos distintos, y cada uno necesita feedback entendible.

El error comun: pintar el formulario y perder el foco

El patron frágil suele ser este:

  1. El usuario envia el formulario.
  2. El servidor responde con varios errores.
  3. React muestra mensajes nuevos y cambia clases CSS.
  4. El foco queda en el boton o salta a un lugar dificil de predecir.

Visualmente parece que todo esta bien porque los mensajes aparecen. Pero la persona que navega con teclado no siempre sabe que aparecieron. Y si el mensaje se inserta encima del campo, el contenido puede moverse justo cuando iba a leerlo.

La solucion es separar tres decisiones: un resumen para orientar, un mensaje junto al campo para explicar y una regla explicita para el foco. En un flujo corto, mover el foco al primer campo invalido suele ser razonable. En un formulario largo, el resumen puede recibir el foco y ofrecer enlaces a cada error.

Un contrato pequeño para errores en React

No hace falta una libreria para empezar. Un estado de errores con claves estables ya permite que el componente sea predecible:

type FormErrors = Record<string, string>;

function SignupForm() {
  const [errors, setErrors] = useState<FormErrors>({});
  const emailRef = useRef<HTMLInputElement>(null);

  async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const nextErrors = await validateSignup();
    setErrors(nextErrors);

    if (nextErrors.email) {
      emailRef.current?.focus();
    }
  }

  return (
    <form onSubmit={handleSubmit} noValidate>
      <div role="alert" aria-live="assertive">
        {Object.keys(errors).length > 0 && "Revisa los campos marcados."}
      </div>
      <label htmlFor="email">Email</label>
      <input
        ref={emailRef}
        id="email"
        name="email"
        type="email"
        aria-invalid={Boolean(errors.email)}
        aria-describedby={errors.email ? "email-error" : undefined}
      />
      {errors.email && <p id="email-error">{errors.email}</p>}
      <button type="submit">Crear cuenta</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

El ejemplo es intencionalmente pequeño. En una aplicación real, la validacion asincrona debe protegerse contra respuestas fuera de orden y el foco solo debe moverse cuando el usuario envio el formulario, no en cada pulsación. Para errores de servidor, mantengo el mismo formato que para errores locales: el frontend no deberia adivinar si un email ya existe o si el servicio esta temporalmente caido.

Este enfoque tambien deja mejor evidencia para QA. Si se prueban varias direcciones, incluidas notas como dummy e mail en una fixture, cada resultado puede decir que campo fallo y que anuncio se mostro. Registrar intentos de email sin perder contexto ayuda a conectar ese resultado visual con el evento que lo provoco.

CSS que mantiene estable la interfaz

El CSS no debe convertir un error en un salto de pantalla. Reserva espacio para la ayuda cuando el diseño lo permita y evita usar solo color para comunicar el estado:

.field-message {
  min-height: 1.4rem;
  color: #b42318;
}

.field[aria-invalid="true"] input {
  border-color: #b42318;
  outline: 2px solid transparent;
}

.field[aria-invalid="true"] input:focus-visible {
  outline: 2px solid #1d4ed8;
  outline-offset: 2px;
}
Enter fullscreen mode Exit fullscreen mode

El min-height reduce reflow cuando el mensaje aparece. El borde comunica el estado, pero el texto y aria-invalid son la informacion principal. En pantallas pequeñas, pruebo el flujo con zoom y con teclado antes de darlo por terminado; a veces un detalle que parecia estable se rompe despues de un mensaje largo.

Como probar teclado, lector de pantalla y reintentos

Mi checklist basico es:

  • Enviar el formulario vacio y confirmar que el foco queda en un lugar util.
  • Corregir solo un campo y verificar que el anuncio no repite errores viejos.
  • Usar Tab y Shift+Tab sin que el foco desaparezca.
  • Comprobar que cada mensaje esta asociado a su input.
  • Simular una respuesta lenta y otra respuesta fuera de orden.
  • Revisar que los errores de servidor no muevan todos los elementos.

Tambien miro el rendimiento: un aria-live demasiado ruidoso, renders innecesarios o validaciones caras en cada tecla hacen que el formulario se sienta lento aunque el servidor responda rapido. Para una interfaz de correo temporal o cualquier signup, esa diferencia se nota mas de lo que parece.

Q&A rapido

¿Debo mover siempre el foco al primer error?

No siempre. En formularios pequeños funciona bien. En formularios largos, un resumen enfocable con enlaces puede conservar mejor el contexto y evitar un salto inesperado.

¿aria-live reemplaza el mensaje visible?

No. Sirve para anunciar cambios, pero el mensaje junto al campo sigue siendo util para quien revisa la pantalla con calma.

¿Que hago con un error generico del servidor?

Muestralo cerca de la accion que fallo y ofrece un siguiente paso concreto. "Algo salio mal" deja a la persona igual que antes, y es dificil de probar.

Un formulario accesible no necesita mas decoracion, necesita estados coherentes. Cuando el foco, el texto, el CSS y los eventos cuentan la misma historia, la interfaz se siente mas rapida y mucho menos frustrante.

Top comments (0)