Mejorando la Accesibilidad
En el capítulo anterior, vimos cómo capturar errores (incluyendo errores 404) y mostrar una alternativa al usuario. Sin embargo, aún nos falta discutir otra pieza del rompecabezas: la validación de formularios. Veamos cómo implementar validación en el servidor con Acciones del Servidor (Server Actions), y cómo puedes mostrar errores de formulario usando el hook useActionState
de React - ¡todo esto manteniendo la accesibilidad en mente!
¿Qué es la accesibilidad?
La accesibilidad se refiere al diseño e implementación de aplicaciones web que todos puedan usar, incluyendo personas con discapacidades. Es un tema amplio que cubre muchas áreas, como navegación por teclado, HTML semántico, imágenes, colores, videos, etc.
Aunque no profundizaremos en accesibilidad en este curso, discutiremos las características de accesibilidad disponibles en Next.js y algunas prácticas comunes para hacer tus aplicaciones más accesibles.
Si deseas aprender más sobre accesibilidad, recomendamos el curso Learn Accessibility de web.dev.
Usando el plugin de accesibilidad ESLint en Next.js
Next.js incluye el plugin eslint-plugin-jsx-a11y
en su configuración ESLint para ayudar a detectar problemas de accesibilidad temprano. Por ejemplo, este plugin advierte si tienes imágenes sin texto alt
, usas atributos aria-*
y role
incorrectamente, entre otros.
Opcionalmente, si deseas probarlo, agrega next lint
como un script en tu archivo package.json
:
Luego ejecuta pnpm lint
en tu terminal:
Esto te guiará a través de la instalación y configuración de ESLint para tu proyecto. Si ejecutaras pnpm lint
ahora, deberías ver el siguiente resultado:
Sin embargo, ¿qué pasaría si tuvieras una imagen sin texto alt
? ¡Vamos a descubrirlo!
Ve a /app/ui/invoices/table.tsx
y elimina el prop alt
de la imagen. Puedes usar la función de búsqueda de tu editor para encontrar rápidamente el <Image>
:
Ahora ejecuta pnpm lint
nuevamente, y deberías ver la siguiente advertencia:
Aunque agregar y configurar un linter no es un paso obligatorio, puede ser útil para detectar problemas de accesibilidad en tu proceso de desarrollo.
Mejorando la accesibilidad de formularios
Hay tres cosas que ya estamos haciendo para mejorar la accesibilidad en nuestros formularios:
- HTML semántico: Usar elementos semánticos (
<input>
,<option>
, etc) en lugar de<div>
. Esto permite que las tecnologías de asistencia (AT) se enfoquen en los elementos de entrada y proporcionen información contextual adecuada al usuario, haciendo el formulario más fácil de navegar y entender. - Etiquetado: Incluir
<label>
y el atributohtmlFor
asegura que cada campo del formulario tenga una etiqueta de texto descriptiva. Esto mejora el soporte para AT al proporcionar contexto y también mejora la usabilidad al permitir que los usuarios hagan clic en la etiqueta para enfocar el campo de entrada correspondiente. - Contorno de enfoque: Los campos están correctamente estilizados para mostrar un contorno cuando están enfocados. Esto es crítico para la accesibilidad ya que indica visualmente el elemento activo en la página, ayudando tanto a usuarios de teclado como de lectores de pantalla a entender dónde están en el formulario. Puedes verificarlo presionando
tab
.
Estas prácticas establecen una buena base para hacer tus formularios más accesibles para muchos usuarios. Sin embargo, no abordan la validación de formularios y los errores.
Validación de formularios
Ve a http://localhost:3000/dashboard/invoices/create, y envía un formulario vacío. ¿Qué sucede?
¡Obtienes un error! Esto ocurre porque estás enviando valores vacíos del formulario a tu Acción del Servidor (Server Action). Puedes prevenir esto validando tu formulario en el cliente o en el servidor.
Validación en el cliente
Hay un par de formas en que puedes validar formularios en el cliente. La más simple sería confiar en la validación de formularios proporcionada por el navegador agregando el atributo required
a los elementos <input>
y <select>
en tus formularios. Por ejemplo:
Envía el formulario nuevamente. El navegador mostrará una advertencia si intentas enviar un formulario con valores vacíos.
Este enfoque generalmente está bien porque algunas ATs soportan la validación del navegador.
Una alternativa a la validación en el cliente es la validación en el servidor. Veamos cómo puedes implementarla en la siguiente sección. Por ahora, elimina los atributos required
si los agregaste.
Validación del lado del servidor (Server-Side validation)
Al validar formularios en el servidor, puedes:
- Asegurar que tus datos tengan el formato esperado antes de enviarlos a tu base de datos.
- Reducir el riesgo de que usuarios malintencionados eviten la validación del lado del cliente (client-side validation).
- Tener una única fuente de verdad sobre lo que se considera datos válidos.
En tu componente create-form.tsx
, importa el hook useActionState
de react
. Como useActionState
es un hook, necesitarás convertir tu formulario en un Componente de Cliente (Client Component) usando la directiva "use client"
:
Dentro de tu Componente de Formulario, el hook useActionState
:
- Toma dos argumentos:
(action, initialState)
. - Retorna dos valores:
[state, formAction]
- el estado del formulario y una función que se llama cuando se envía el formulario.
Pasa tu acción createInvoice
como argumento de useActionState
, y dentro del atributo <form action={}>
, llama a formAction
.
El initialState
puede ser cualquier cosa que definas. En este caso, crea un objeto con dos claves vacías: message
y errors
, e importa el tipo State
de tu archivo actions.ts
. State
aún no existe, pero lo crearemos a continuación:
Esto puede parecer confuso al principio, pero tendrá más sentido una vez que actualices la acción del servidor. Hagámoslo ahora.
En tu archivo action.ts
, puedes usar Zod para validar los datos del formulario. Actualiza tu FormSchema
de la siguiente manera:
customerId
- Zod ya lanza un error si el campo del cliente está vacío, ya que espera un tipostring
. Pero agreguemos un mensaje amigable si el usuario no selecciona un cliente.amount
- Como estás convirtiendo el tipo de monto destring
anumber
, por defecto será cero si el string está vacío. Indiquémosle a Zod que siempre queremos que el monto sea mayor que 0 con la función.gt()
.status
- Zod ya lanza un error si el campo de estado está vacío, ya que espera "pending" o "paid". También agreguemos un mensaje amigable si el usuario no selecciona un estado.
A continuación, actualiza tu acción createInvoice
para aceptar dos parámetros: prevState
y formData
:
formData
- igual que antes.prevState
- contiene el estado pasado desde el hookuseActionState
. No lo usarás en la acción en este ejemplo, pero es una propiedad requerida.
Luego, cambia la función parse()
de Zod a safeParse()
:
safeParse()
retornará un objeto que contiene un campo success
o error
. Esto ayudará a manejar la validación de manera más elegante sin tener que poner esta lógica dentro de un bloque try/catch
.
Antes de enviar la información a tu base de datos, verifica si los campos del formulario se validaron correctamente con un condicional:
Si validatedFields
no es exitoso, retornamos la función temprano con los mensajes de error de Zod.
Consejo: console.log
validatedFields
y envía un formulario vacío para ver su estructura.
Finalmente, como estás manejando la validación del formulario por separado, fuera de tu bloque try/catch, puedes retornar un mensaje específico para cualquier error de base de datos. Tu código final debería verse así:
Genial, ahora mostremos los errores en tu componente de formulario. De vuelta en el componente create-form.tsx
, puedes acceder a los errores usando el state
del formulario.
Agrega un operador ternario que verifique cada error específico. Por ejemplo, después del campo del cliente, puedes agregar:
Consejo: Puedes hacer console.log de
state
dentro de tu componente y verificar si todo está conectado correctamente. Revisa la consola en Dev Tools ya que tu formulario ahora es un Componente de Cliente.
En el código anterior, también estás agregando las siguientes etiquetas aria:
aria-describedby="customer-error"
: Establece una relación entre el elementoselect
y el contenedor de mensajes de error. Indica que el contenedor conid="customer-error"
describe el elementoselect
. Los lectores de pantalla leerán esta descripción cuando el usuario interactúe con el cuadroselect
para notificarles los errores.id="customer-error"
: Este atributoid
identifica de manera única el elemento HTML que contiene el mensaje de error para el inputselect
. Esto es necesario para quearia-describedby
establezca la relación.aria-live="polite"
: El lector de pantalla debe notificar cortésmente al usuario cuando se actualice el error dentro deldiv
. Cuando el contenido cambie (por ejemplo, cuando un usuario corrija un error), el lector de pantalla anunciará estos cambios, pero solo cuando el usuario esté inactivo para no interrumpirlo.
Práctica: Agregar etiquetas aria
Usando el ejemplo anterior, agrega errores a los campos restantes de tu formulario. También deberías mostrar un mensaje al final del formulario si faltan campos. Tu interfaz debería verse así:

Una vez que estés listo, ejecuta pnpm lint
para verificar si estás usando las etiquetas aria correctamente.
Si quieres desafiarte, toma el conocimiento que has aprendido en este capítulo y agrega validación de formulario al componente edit-form.tsx
.
Necesitarás:
- Agregar
useActionState
a tu componenteedit-form.tsx
. - Editar la acción
updateInvoice
para manejar errores de validación de Zod. - Mostrar los errores en tu componente y agregar etiquetas aria para mejorar la accesibilidad.
Una vez que estés listo, expande el siguiente fragmento de código para ver la solución: