Formularios Accesibles
Crea formularios utilizables por todos con una experiencia clara y guiada
Etiquetas y asociaciones
Mal ejemplo
<!-- ❌ Malo: Etiquetas faltantes -->
<form>
<h4>Iniciar sesión</h4>
<!-- Sin etiqueta asociada -->
<input
type="email"
placeholder="Tu correo electrónico">
<!-- Sin etiqueta asociada, sin autocompletado -->
<input
type="password"
placeholder="Contraseña">
<!-- Casilla de verificación no asociada -->
<input type="checkbox">
<span>Recuérdame</span>
<button type="submit">Iniciar sesión</button>
</form>Buen ejemplo
<!-- ✅ Bueno: Etiquetas explícitas y asociadas -->
<form>
<h4>Iniciar sesión</h4>
<!-- Etiqueta asociada mediante for/id -->
<label for="email-good">Dirección de correo electrónico</label>
<input
type="email"
id="email-good"
placeholder="example@domain.com"
required
aria-describedby="email-help"
autocomplete="email">
<div id="email-help">
Nunca compartiremos tu correo electrónico
</div>
<!-- Etiqueta con instrucciones -->
<label for="password-good">Contraseña</label>
<input
type="password"
id="password-good"
required
aria-describedby="password-help"
autocomplete="current-password">
<div id="password-help">
Mínimo 8 caracteres con mayúsculas y números
</div>
<!-- Casilla de verificación correctamente asociada -->
<input type="checkbox" id="remember-good">
<label for="remember-good">Recuérdame</label>
<button type="submit">Iniciar sesión</button>
</form>Explicación: Cada campo de formulario debe tener una etiqueta explícita y correctamente asociada. Los marcadores de posición no reemplazan las etiquetas.
Validación y mensajes de error
Mal ejemplo
<!-- ❌ Malo: Errores mal anunciados -->
<form @submit.prevent="submitBadForm">
<h4>Registro a newsletter</h4>
<!-- Sin asociación etiqueta-entrada -->
<label>Nombre</label>
<input
type="text"
v-model="badForm.name"
:class="{ 'error': submitted && !badForm.name }">
<!-- Solo error visual -->
<label>Correo electrónico</label>
<input
type="email"
v-model="badForm.email"
:class="{ 'error': submitted && !isValidEmail(badForm.email) }">
<!-- Sin información de error -->
<label>Edad</label>
<input
type="number"
v-model="badForm.age"
:class="{ 'error': submitted && badForm.age < 16 }">
<!-- Mensaje de error genérico e inútil -->
<div v-if="submitted && badFormErrors.length">
❌ Por favor corrija los errores
</div>
<button type="submit">Registrarse</button>
</form>Buen ejemplo
<!-- ✅ Bueno: Errores claramente asociados -->
<form @submit.prevent="submitGoodForm" novalidate>
<h4>Registro a newsletter</h4>
<!-- Resumen de errores con navegación -->
<div
v-if="submitted && goodFormErrors.length"
role="alert"
aria-labelledby="error-summary-title">
<h5 id="error-summary-title">Errores a corregir:</h5>
<ul>
<li v-for="error in goodFormErrors" :key="error.field">
<a :href="`#${error.field}-good-form`">
</a>
</li>
</ul>
</div>
<!-- Campo con error específico asociado -->
<label for="name-good-form">Nombre completo *</label>
<input
type="text"
id="name-good-form"
v-model="goodForm.name"
required
aria-invalid="submitted && !goodForm.name"
aria-describedby="name-error">
<div
v-if="submitted && !goodForm.name"
id="name-error"
role="alert">
❌ El nombre es obligatorio
</div>
<button type="submit">Registrarse</button>
</form>Explicación: Los errores deben ser claramente anunciados, asociados con los campos relevantes y proporcionar instrucciones precisas para la corrección.
Grupos de campos con aria-labelledby
Mal ejemplo
<!-- ❌ Malo: Estructura desorganizada -->
<form>
<h4>Preferencias</h4>
<!-- Elementos mezclados sin lógica -->
<div>
<input type="radio" name="contact-bad"
id="email-contact-bad" value="email">
<label for="email-contact-bad">Correo electrónico</label>
<input type="checkbox" id="newsletter-bad"
value="newsletter">
<label for="newsletter-bad">Newsletter</label>
</div>
<div>
<input type="radio" name="contact-bad"
id="phone-contact-bad" value="phone">
<label for="phone-contact-bad">Teléfono</label>
<!-- Campo de texto sin contexto -->
<input type="text" placeholder="Nombre">
</div>
<div>
<input type="checkbox" id="offers-bad" value="offers">
<label for="offers-bad">Ofertas</label>
<input type="radio" name="contact-bad"
id="sms-contact-bad" value="sms">
<label for="sms-contact-bad">SMS</label>
</div>
<!-- Select sin contexto -->
<select>
<option>Elegir...</option>
<option>Diario</option>
<option>Semanal</option>
</select>
<button type="submit">OK</button>
</form>Buen ejemplo
<!-- ✅ Bueno: Grupos lógicos con aria-labelledby -->
<form>
<h4>Preferencias de contacto</h4>
<!-- Sección de información personal -->
<div role="group" aria-labelledby="personal-info-title">
<h5 id="personal-info-title">
Información personal
</h5>
<label for="name-good">Nombre completo *</label>
<input
type="text"
id="name-good"
name="name"
required
aria-required="true"
aria-describedby="name-desc">
<span id="name-desc">
Tu nombre completo como aparecerá en nuestras comunicaciones
</span>
</div>
<!-- Grupo de botones de radio -->
<div
role="radiogroup"
aria-labelledby="contact-method-title"
aria-required="true">
<h5 id="contact-method-title">
Método de contacto preferido *
</h5>
<input
type="radio"
name="contact-good"
id="email-contact-good"
value="email"
required>
<label for="email-contact-good">Correo electrónico</label>
<input
type="radio"
name="contact-good"
id="phone-contact-good"
value="phone"
required>
<label for="phone-contact-good">Teléfono</label>
</div>
<!-- Grupo de casillas de verificación -->
<div role="group" aria-labelledby="comm-types-title">
<h5 id="comm-types-title">Tipos de comunicación</h5>
<p id="comm-types-desc">
Selecciona los tipos de comunicaciones que deseas recibir
</p>
<input
type="checkbox"
id="newsletter-good"
value="newsletter"
aria-describedby="comm-types-desc">
<label for="newsletter-good">Newsletter</label>
</div>
<button type="submit">
Guardar preferencias
</button>
</form>Explicación: Los grupos de campos relacionados deben estar asociados con sus títulos mediante aria-labelledby para crear relaciones semánticas claras, particularmente para botones de radio y casillas de verificación.
Formulario complejo con validación en tiempo real
Mal ejemplo
<!-- ❌ Malo: Validación intrusiva -->
<form>
<h4>Crear cuenta</h4>
<!-- Validación inmediata en cada pulsación -->
<label>Nombre de usuario</label>
<input
type="text"
v-model="complexBadForm.username"
@input="validateUsernameBad"
:class="{ 'error': usernameBadError }">
<!-- Error que aparece/desaparece constantemente -->
<div v-if="usernameBadError">
</div>
<!-- Sin asociación con aria-describedby -->
<label>Correo electrónico</label>
<input
type="email"
v-model="complexBadForm.email"
@input="validateEmailBad"
:class="{ 'error': emailBadError }">
<div v-if="emailBadError"></div>
<!-- Mensajes de error demasiado cortos y no útiles -->
<!-- Sin autocompletado para ayudar a los gestores de contraseñas -->
<label>Contraseña</label>
<input
type="password"
v-model="complexBadForm.password"
@input="validatePasswordBad"
:class="{ 'error': passwordBadError }">
<div v-if="passwordBadError"></div>
<button type="submit">Crear cuenta</button>
</form>Buen ejemplo
<!-- ✅ Bueno: Validación respetuosa con retroalimentación -->
<form>
<h4>Crear cuenta</h4>
<!-- Validación en blur, no en input -->
<label for="username-complex">Nombre de usuario *</label>
<input
type="text"
id="username-complex"
v-model="complexGoodForm.username"
@blur="validateUsernameGood"
:class="{ error: usernameGoodError,
success: usernameGoodValid }"
aria-describedby="username-requirements username-feedback"
aria-invalid="!!usernameGoodError"
required>
<!-- Instrucciones claras -->
<div id="username-requirements">
<h6>Requisitos:</h6>
<ul>
<li :class="{ valid: complexGoodForm.username.length >= 3 }">
Al menos 3 caracteres
</li>
<li :class="{ valid: /^[a-zA-Z0-9_]+$/.test(username) }">
Solo letras, números y guión bajo
</li>
</ul>
</div>
<!-- Retroalimentación accesible -->
<div
v-if="usernameGoodError"
id="username-feedback"
role="alert"
aria-live="polite">
❌
</div>
<div
v-else-if="usernameGoodValid"
id="username-feedback"
role="status"
aria-live="polite">
✅ Nombre de usuario disponible
</div>
<!-- Indicador de fortaleza de contraseña -->
<label for="password-complex">Contraseña *</label>
<input
type="password"
id="password-complex"
v-model="complexGoodForm.password"
@input="validatePasswordGood"
aria-describedby="password-strength"
autocomplete="new-password"
required>
<div id="password-strength">
<h6>Fortaleza de la contraseña:</h6>
<div class="strength-indicator">
<div
class="strength-bar"
:class="`strength-${passwordStrength}`"
:style="{ width: `${passwordStrength * 25}%` }">
</div>
</div>
</div>
<button type="submit" :disabled="!isFormValid">
Crear cuenta
</button>
</form>Explicación: Para formularios complejos, la validación en tiempo real puede ayudar, pero debe implementarse de manera no intrusiva y accesible.