Etiquetas y asociaciones

Mal ejemplo

Iniciar sesión

Recuérdame
<!-- ❌ 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

Iniciar sesión

Nunca compartiremos tu correo electrónico
Mínimo 8 caracteres con mayúsculas y números
<!-- ✅ 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

Registro a newsletter

<!-- ❌ 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

Registro a newsletter

Debes tener al menos 16 años
<!-- ✅ 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

Preferencias

<!-- ❌ 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

Preferencias de contacto

Información personal
Tu nombre completo como aparecerá en nuestras comunicaciones
Usaremos este correo para todas nuestras comunicaciones
Método de contacto preferido *
Tipos de comunicación

Selecciona los tipos de comunicaciones que deseas recibir

Frecuencia de comunicación
Establece la frecuencia máxima de nuestros envíos
<!-- ✅ 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

Crear cuenta

<!-- ❌ 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

Crear cuenta

Requisitos:
  • Al menos 3 caracteres
  • Solo letras, números y guión bajo
Fortaleza de la contraseña:
8+ caracteres
Mayúscula
Número
Carácter especial
<!-- ✅ 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.