Lección 7 de 10 · 12 min de lectura · Revisado el 26/10/2026
Casillas, interruptores, archivos y otros controles especiales
Un formulario real no está hecho solo de cuadros de texto. Tiene casillas para aceptar condiciones, interruptores de preferencias, un botón para adjuntar el certificado de empadronamiento, un deslizador para elegir un importe, un contador de caracteres en el campo de observaciones. Cada uno de estos controles tiene su semántica, su comportamiento de teclado y sus anuncios, y cada uno se rompe de una manera característica cuando se rediseña. En esta lección los repasamos con la misma regla que aplicaremos a los selects y calendarios en la lección siguiente: primero el control nativo, y solo si no basta, el patrón de la ARIA Authoring Practices Guide (se abre en una ventana nueva) (APG) completo. Los componentes interactivos genéricos (menús, pestañas, diálogos) se tratan en el curso N3-C5.
Casillas y botones de opción con estilo propio
La petición más habitual del diseño es «quiero que las casillas tengan nuestro estilo». La respuesta accesible casi nunca es construirlas con <div>: es conservar el <input> nativo y vestirlo. Hay tres formas, de menos a más trabajo:
accent-color: una sola propiedad CSS que cambia el color de casillas, botones de opción, deslizadores y barras de progreso nativos. Funciona en todos los navegadores actuales y no toca la semántica.appearance: nonesobre el propio<input>y dibujo del estado con CSS. El control sigue siendo nativo: recibe el foco, responde a la barra espaciadora y anuncia «marcado» o «no marcado».- Input visualmente oculto (con la técnica de ocultación visual, nunca con
display: nonenivisibility: hidden) y un elemento decorativo al lado que se pinta según:checkedy:focus-visibledel input.
Ejemplo
Incorrecto: una casilla que solo existe para el ratón.
<div class="check" onclick="this.classList.toggle('on')"></div>
<span>Acepto la política de privacidad</span>Correcto: nativa, con el color corporativo y la etiqueta asociada.
<input type="checkbox" id="privacidad" name="privacidad" required>
<label for="privacidad">He leído y acepto la
<a href="/privacidad">política de privacidad</a> (obligatorio)</label>
<style> input[type="checkbox"] { accent-color: #1a4fa0; } </style>Recuerda el comportamiento de teclado que dan gratis los controles nativos y que cualquier sustituto debe reproducir: en las casillas, Tab va de una a otra y la barra espaciadora marca o desmarca; en los botones de opción, Tab entra en el grupo (en la opción marcada o en la primera) y las flechas mueven la selección dentro del grupo. Los botones de opción se agrupan por su atributo name: si cada uno tiene un name distinto, dejan de comportarse como grupo.
Grupos de casillas obligatorios
El atributo required en una casilla significa que esa casilla debe marcarse; no sirve para «elige al menos una opción». Para ese caso, indica la regla en la <legend> («Idiomas en los que puedes atender (elige al menos uno)»), valida con JavaScript y, si falla, muestra el mensaje junto al grupo y vincúlalo al <fieldset> con aria-describedby. Como el soporte de las descripciones en grupos varía entre lectores, el resumen de errores de la lección de validación sigue siendo la red de seguridad.
Interruptores
Un interruptor (switch) representa algo que está activado o desactivado y que surte efecto de inmediato, como «Recibir avisos por SMS» en el área personal. Una casilla, en cambio, es una elección que se envía con el formulario. Si el control aplica el cambio al pulsarlo, usa el rol switch:
<button type="button" role="switch" aria-checked="false" id="sms">
Avisos por SMS
</button>
<p role="status" id="sms-estado"></p>
<script>
const sw = document.getElementById('sms');
sw.addEventListener('click', () => {
const on = sw.getAttribute('aria-checked') === 'true';
sw.setAttribute('aria-checked', String(!on));
document.getElementById('sms-estado').textContent =
!on ? 'Avisos por SMS activados' : 'Avisos por SMS desactivados';
});
</script>El lector anuncia «Avisos por SMS, interruptor, desactivado». El texto del botón no cambia: lo que cambia es el estado. La confirmación en role="status" es opcional, pero útil cuando el cambio se guarda en el servidor. Visualmente, muestra el estado también con texto («Sí» / «No») o con la posición del mando, no solo con el color. El patrón completo está en la APG.
Error frecuente
Un botón que cambia su texto de «Activar avisos» a «Desactivar avisos» y además lleva aria-pressed. El lector anuncia «Desactivar avisos, botón de alternancia, pulsado» y nadie sabe si los avisos están activos o no. Elige una sola estrategia: o un nombre fijo con estado (role="switch" o aria-pressed), o un texto que cambia sin estado.
Adjuntar archivos
En las sedes electrónicas, adjuntar documentos es parte de casi cualquier trámite, y es uno de los puntos donde más solicitudes se abandonan. El <input type="file"> nativo es accesible con teclado y lector de pantalla; los problemas llegan con las zonas de «arrastra aquí tus archivos».
- Arrastrar no puede ser la única vía: debe haber un botón de selección que funcione con teclado (2.1.1 Teclado) y, por 2.5.7 Movimientos de arrastre (AA), una alternativa con un solo puntero sin arrastre.
- El atributo
acceptfiltra el selector de archivos, pero no explica nada. Los formatos y el tamaño máximo se escriben en la ayuda del campo y se asocian conaria-describedby. - El progreso de la subida se muestra con
<progress>etiquetado y se anuncia en pocos hitos (inicio, final, error) con una región de estado, no en cada porcentaje. - Cada archivo subido aparece en una lista con su nombre y un botón «Eliminar» que diga cuál elimina.
- Los errores son concretos: «El archivo pesa 12 MB y el máximo es 10 MB», no «Archivo no válido».
<label for="padron">Certificado de empadronamiento</label>
<p id="padron-ayuda">PDF, JPG o PNG. Máximo 10 MB.</p>
<input type="file" id="padron" name="padron"
accept=".pdf,.jpg,.jpeg,.png" aria-describedby="padron-ayuda">
<ul id="adjuntos">
<li>padron-2026.pdf (240 kB)
<button type="button">Eliminar <span class="visualmente-oculto">padron-2026.pdf</span></button>
</li>
</ul>
<p role="status" id="adjuntos-estado"></p>Deslizadores, cantidades y contadores
Deslizadores
El <input type="range"> nativo es operable con flechas, pero anuncia números sin contexto («25000»). Añade aria-valuetext con el valor legible («25.000 euros») y actualízalo al cambiar. Y como afinar un valor exacto con un deslizador es difícil para muchas personas, en importes o plazos ofrece al lado un campo de texto sincronizado. Un deslizador hecho a mano exige el patrón slider de la APG, con aria-valuenow, aria-valuemin y aria-valuemax.
Selectores de cantidad
Los botones «−» y «+» del carrito necesitan nombre completo («Reducir cantidad de Mochila escolar azul»), un objetivo de al menos 24 × 24 píxeles y, tras pulsarlos, un anuncio del nuevo valor en una región de estado si el foco no está sobre el campo. Permite también escribir la cantidad directamente.
Contadores de caracteres
Indica el límite en la descripción del campo («Máximo 500 caracteres») y muestra lo que queda. No anuncies cada pulsación: actualiza el texto visible al momento, pero envía el anuncio a la región activa con un pequeño retardo, cuando la persona deja de escribir, o al cruzar umbrales. Evita también que maxlength recorte en silencio un texto pegado: es preferible dejar escribir de más y mostrar un error claro con lo que sobra.
Punto clave
Cada control especial tiene un control nativo que ya resuelve la mayor parte del trabajo: checkbox, radio, file, range, <progress>. Tu trabajo es vestirlo, explicarlo (formatos, límites, valor legible) y anunciar con mesura lo que cambia sin mover el foco.
Pruébalo tú
Busca un formulario con casillas o interruptores personalizados (las preferencias de cookies de muchas webs son un buen candidato). Recórrelo solo con teclado: ¿llegas a cada control con Tab? ¿La barra espaciadora cambia el estado? Inspecciona uno en el panel Accessibility del navegador: ¿qué rol y qué estado expone?
Resumen
- Para dar estilo a casillas y botones de opción, conserva el input nativo:
accent-color,appearance: noneu ocultación visual, nuncadisplay: none. requireden una casilla obliga a marcar esa casilla; «al menos una» se valida por script y se explica en la<legend>.- Un interruptor de efecto inmediato usa
role="switch"conaria-checkedy un nombre que no cambia. - Subir archivos exige botón además de arrastre, formatos y tamaño explicados, progreso anunciado con mesura y errores concretos.
- Deslizadores con
aria-valuetexty campo alternativo; contadores que no anuncian cada tecla ni recortan en silencio.
Comprueba lo que has aprendido
Esta lección se puede leer libremente. Para hacer su test de 3 preguntas, los ejercicios prácticos con páginas reales, guardar tu avance y optar al examen y al certificado, matricúlate en el curso.