Lección 2 de 10 · 17 min de lectura · Revisado el 26/10/2026
Botones, enlaces y controles personalizados
Botones y enlaces son los dos controles más abundantes de cualquier web y, paradójicamente, los que más se confunden entre sí. Para una persona que ve la pantalla la diferencia es cosmética; para quien usa lector de pantalla o control por voz la diferencia es funcional: un enlace «va a un sitio», un botón «hace algo», y cada uno se opera con teclas distintas y aparece en listas distintas. Esta lección fija cuándo usar cada uno, cómo nombrarlos y qué hay que reconstruir cuando, excepcionalmente, se construye un control desde cero.
Botón o enlace: la decisión
| Si la acción… | Usa | Teclado nativo |
|---|---|---|
| Navega a otra URL o a otra parte de la página | <a href="…"> | Intro |
| Ejecuta algo en la página (abrir, enviar, borrar, alternar) | <button type="button"> | Intro y Espacio |
| Envía un formulario | <button type="submit"> | Intro y Espacio |
| Descarga un archivo | <a href="…" download> | Intro |
Las consecuencias de equivocarse son concretas. Un enlace con href="#" que abre un modal aparece en la lista de enlaces del lector de pantalla, ofrece «abrir en pestaña nueva» en el menú contextual (lo que abre una página vacía), no responde a Espacio y, si JavaScript falla, hace saltar la página arriba. Un <button> que navega con onclick="location.href=…" no se puede abrir en otra pestaña, no muestra la URL de destino y no aparece en la lista de enlaces.
Error frecuente
<a href="#" onclick="abrirCarrito()">Ver carrito</a>
<a href="javascript:void(0)" class="btn">Aceptar</a>
<a class="btn">Continuar</a>Los dos primeros son botones disfrazados de enlace. El tercero es peor: un <a> sin href no es enfocable ni tiene rol de enlace, así que ni siquiera se puede llegar a él con teclado. Todos deberían ser <button type="button">.
Si por restricciones del framework o del CMS no puedes cambiar el elemento, aplica el parche completo: role="button", manejador de Espacio (porque un enlace no lo tiene) y href válido. Pero es un parche, no la solución.
Nombres que funcionan fuera de contexto
Las personas usuarias de lectores de pantalla navegan a menudo con la lista de enlaces o de botones, donde cada nombre aparece sin el texto que lo rodea. El criterio 2.4.4 Propósito de los enlaces (en contexto) (A) permite que el propósito se deduzca del contexto programático (la frase o el elemento de lista que lo contiene), pero la práctica recomendable, y lo que exige el AAA 2.4.9, es que el texto baste por sí solo.
- «Leer más» repetido diez veces no sirve. Mejor: «Leer más sobre las tasas 2025», aunque parte del texto quede visualmente oculto con una clase de ocultación accesible, o un
aria-labelledbyque combine el texto del enlace con el título de la tarjeta. - «Pulse aquí» o «este enlace» no describen nada.
- En enlaces que abren en una nueva pestaña o descargan un archivo, indica el comportamiento y el formato: «Memoria 2024 (PDF, 2 MB)», «Portal de transparencia (se abre en una ventana nueva)».
- En botones con icono, el nombre describe la acción («Cerrar», «Buscar», «Eliminar la fila de Ana García»), no el dibujo («Equis», «Lupa», «Papelera»).
Ejemplo
Tarjeta de noticia con un enlace genérico que se vuelve específico sin cambiar el diseño:
<article>
<h3 id="n-42">Nuevas ayudas al alquiler para menores de 35 años</h3>
<p>…</p>
<a href="/noticias/42" aria-labelledby="leer-42 n-42" id="leer-42">Leer más</a>
</article>El nombre accesible pasa a ser «Leer más Nuevas ayudas al alquiler para menores de 35 años». Cumple 2.5.3 porque el texto visible («Leer más») está contenido en el nombre.
Estados de los botones
- Deshabilitado:
disabledquita el botón del orden de tabulación y lo anuncia como «no disponible». Es correcto para acciones imposibles en ese momento, pero tiene un coste: la persona no puede llegar a él para saber que existe ni por qué está deshabilitado. Para el botón de envío de un formulario, muchas guías recomiendan dejarlo habilitado y mostrar los errores al pulsarlo, en lugar de deshabilitarlo hasta que todo sea válido. Si necesitas que siga siendo enfocable, usaaria-disabled="true"e ignora el clic en JavaScript. - Conmutador (activado/desactivado):
aria-pressed="true|false". El nombre no cambia («Negrita»); cambia el estado. No pongas «Activar negrita» y «Desactivar negrita» alternando el texto y ademásaria-pressed: el lector dirá «Desactivar negrita, pulsado», que es confuso. - Despliega algo:
aria-expandedy, si lo que despliega no es el siguiente elemento,aria-controls. - Abre un menú o diálogo:
aria-haspopup="menu",aria-haspopup="dialog"(opcional, pero informativo). - En curso: mientras se procesa, cambia el texto a «Enviando…» y usa
aria-disabledo una región de estado; evita el spinner mudo.
Controles personalizados: la lista de reconstrucción
Cuando un control no tiene equivalente nativo (un botón de «me gusta» con animación compleja, un selector de color, una ficha arrastrable), hay que reconstruir manualmente lo que el navegador daría. La lista de comprobación mínima:
- Elemento contenedor con el
rolecorrecto de la APG (se abre en una ventana nueva). tabindex="0"(o roving tabindex si forma parte de un grupo).- Nombre accesible visible o con
aria-labelledby/aria-label. - Estados ARIA actualizados por JavaScript en cada cambio.
- Manejador
keydownpara Intro y Espacio (y flechas si el patrón las usa), conpreventDefault()en Espacio. - Indicador de foco visible con
:focus-visible. - Tamaño mínimo del objetivo de 24 por 24 píxeles CSS (2.5.8 Tamaño del objetivo (mínimo)), salvo que haya suficiente separación, un equivalente mayor, o esté dentro de una línea de texto.
- Eventos de puntero que no dependan de gestos complejos (2.5.1) y que puedan cancelarse soltando fuera (2.5.2 Cancelación del puntero): activa en
clickopointerup, no enpointerdown.
Punto clave
Antes de construir un control personalizado, intenta estilizar el nativo. Un <button> admite cualquier CSS, incluidos iconos, animaciones y formas, y te ahorra los ocho puntos de la lista. El control personalizado es la excepción justificada, no el punto de partida.
Cómo comprobarlo
- Abre la lista de enlaces y la de botones con el lector de pantalla: cada nombre debe entenderse solo y estar en la lista correcta.
- Pulsa Espacio sobre cada cosa que parece un botón: si navega o no hace nada, no es un botón.
- Haz clic con el botón derecho sobre lo que parece un enlace: debe ofrecer «abrir en pestaña nueva».
- Mide los objetivos pequeños con las herramientas del navegador: 24 por 24 píxeles CSS como mínimo.
Resumen
- Enlace para navegar, botón para actuar; nunca
href="#"con unonclickni<a>sinhref. - Los nombres deben funcionar fuera de contexto e incluir el texto visible; en iconos, describen la acción.
- Los estados se expresan con
disabled/aria-disabled,aria-pressed,aria-expandedyaria-haspopup, actualizados por JavaScript. - Un control personalizado exige rol,
tabindex, nombre, estados, teclado, foco visible, tamaño mínimo y cancelación del puntero. - Estilizar el elemento nativo casi siempre es más barato y más robusto que reconstruirlo.
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.