Guía detallada

Link Purpose Implementation Guide

Ver contenido

Guía de Implementación de Propósito del Enlace: Patrones Completos para Enlaces Accesibles

Introducción

Crear enlaces accesibles va más allá de simplemente evitar “haga clic aquí.” Esta guía cubre patrones completos para implementar propósito del enlace a través de diferentes contextos—desde enlaces de texto simples hasta patrones complejos de tarjetas, menús de navegación y enlaces solo con icono. Aprenderás técnicas para cumplir tanto los requisitos de WCAG 2.4.4 (Nivel A) como 2.4.9 (Nivel AAA).

Comprender cómo los lectores de pantalla interpretan los enlaces y cómo los usuarios navegan por listas de enlaces es esencial para implementar patrones de enlace verdaderamente accesibles.

Cálculo del Nombre Accesible para Enlaces

Cómo los Enlaces Obtienen Sus Nombres

El nombre accesible para un enlace se calcula en este orden de prioridad:

  1. aria-labelledby - Referencia otros elementos
  2. aria-label - Etiqueta de cadena directa
  3. Contenido de texto del enlace - Texto visible y alt de imágenes
  4. Atributo title - Último recurso (no recomendado)
<!-- Prioridad 1: aria-labelledby -->
<span id="product-name">Producto A</span>
<a href="/productos/a" aria-labelledby="product-name learn-more-1">
  <span id="learn-more-1">Más información</span>
</a>
<!-- Nombre accesible: "Producto A Más información" -->

<!-- Prioridad 2: aria-label -->
<a href="/productos/a" aria-label="Más información sobre Producto A">
  Más información
</a>
<!-- Nombre accesible: "Más información sobre Producto A" -->

<!-- Prioridad 3: Contenido de texto del enlace -->
<a href="/productos/a">Ver detalles del Producto A</a>
<!-- Nombre accesible: "Ver detalles del Producto A" -->

Mejor Práctica: Usar Texto Visible Cuando Sea Posible

<!-- MEJOR: El texto visible es descriptivo -->
<a href="/precios">Ver nuestros planes de precios</a>

<!-- ACEPTABLE: aria-label complementa texto visible corto -->
<a href="/precios" aria-label="Ver planes de precios">
  Precios
</a>

<!-- EVITAR: atributo title para nombre accesible -->
<a href="/precios" title="Ver planes de precios">
  Precios
</a>

Patrones de Enlaces de Texto

Enlaces Autodescriptivos (Nivel AAA)

<!-- Enlaces de navegación -->
<nav aria-label="Navegación principal">
  <a href="/">Inicio</a>
  <a href="/nosotros">Sobre nuestra empresa</a>
  <a href="/servicios">Nuestros servicios</a>
  <a href="/portafolio">Ver nuestro portafolio</a>
  <a href="/contacto">Contáctanos</a>
</nav>

<!-- Enlaces de acción -->
<a href="/registro">Crear tu cuenta gratuita</a>
<a href="/demo">Solicitar una demostración del producto</a>
<a href="/suscribir">Suscribirse a nuestro boletín</a>

<!-- Enlaces de contenido -->
<a href="/blog/guia-accesibilidad">
  Leer: La Guía Completa de Accesibilidad Web
</a>

Enlaces Contextuales (Nivel A)

Cuando el contexto aclara el propósito, texto de enlace más simple es aceptable:

<!-- Contexto de párrafo -->
<p>
  Ofrecemos tres niveles de servicio para satisfacer tus necesidades. Visita nuestra
  <a href="/precios">página de precios</a>
  para comparar opciones.
</p>

<!-- Contexto de lista -->
<ul>
  <li>
    <strong>Auditoría de Accesibilidad</strong> - Revisión completa de tu sitio.
    <a href="/servicios/auditoria">Más información</a>
  </li>
  <li>
    <strong>Investigación UX</strong> - Pruebas de usuario y análisis.
    <a href="/servicios/investigacion">Más información</a>
  </li>
</ul>

Nota: Aunque los enlaces contextuales pasan Nivel A, hacer todos los enlaces autodescriptivos (Nivel AAA) proporciona una mejor experiencia de usuario.

Patrones de Enlaces Solo con Icono

Enlace de Icono Básico

<!-- Usando aria-label -->
<a href="/buscar" class="icon-link" aria-label="Buscar">
  <svg aria-hidden="true" viewBox="0 0 24 24">
    <path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5z"/>
  </svg>
</a>

<!-- Usando texto oculto visualmente -->
<a href="/buscar" class="icon-link">
  <span class="visually-hidden">Buscar</span>
  <svg aria-hidden="true" viewBox="0 0 24 24">
    <path d="..."/>
  </svg>
</a>

<style>
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
</style>

Enlaces de Redes Sociales

<nav aria-label="Redes sociales">
  <a href="https://twitter.com/empresa" aria-label="Síguenos en Twitter">
    <svg aria-hidden="true"><!-- icono de Twitter --></svg>
  </a>
  <a href="https://linkedin.com/company/nombre" aria-label="Conecta en LinkedIn">
    <svg aria-hidden="true"><!-- icono de LinkedIn --></svg>
  </a>
  <a href="https://github.com/empresa" aria-label="Ver nuestro GitHub">
    <svg aria-hidden="true"><!-- icono de GitHub --></svg>
  </a>
</nav>

Enlaces de Icono de Acción

<div class="actions">
  <a href="/editar/123" aria-label="Editar documento">
    <svg aria-hidden="true"><!-- icono de editar --></svg>
  </a>
  <a href="/eliminar/123" aria-label="Eliminar documento">
    <svg aria-hidden="true"><!-- icono de eliminar --></svg>
  </a>
  <a href="/descargar/123" aria-label="Descargar documento">
    <svg aria-hidden="true"><!-- icono de descargar --></svg>
  </a>
</div>

Patrones de Enlaces en Tarjetas

Tarjeta Completa como Enlace

<!-- Enlace de tarjeta simple -->
<a href="/blog/slug-articulo" class="card-link">
  <article class="card">
    <img src="/images/articulo.jpg" alt="">
    <h3>Entendiendo la Accesibilidad Web</h3>
    <p>Aprende los fundamentos de crear sitios web accesibles...</p>
  </article>
</a>

<style>
.card-link {
  display: block;
  text-decoration: none;
  color: inherit;
}

.card-link:hover .card {
  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}

.card-link:focus-visible {
  outline: 2px solid #007bff;
  outline-offset: 2px;
}
</style>

Tarjeta con Múltiples Enlaces

<article class="card">
  <img src="/images/articulo.jpg" alt="">
  <h3>
    <a href="/blog/accesibilidad" class="card-title-link">
      Entendiendo la Accesibilidad Web
    </a>
  </h3>
  <p>Aprende los fundamentos de crear sitios web accesibles...</p>
  <div class="card-meta">
    <a href="/categoria/accesibilidad" class="category-link">
      Accesibilidad
    </a>
    <a href="/autor/maria-garcia" class="author-link">
      Por María García
    </a>
  </div>
</article>

<style>
/* Hacer que el enlace del título cubra toda la tarjeta para facilitar el clic */
.card {
  position: relative;
}

.card-title-link::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}

/* Elevar otros enlaces por encima de la superposición */
.category-link,
.author-link {
  position: relative;
  z-index: 1;
}
</style>

Patrón de “Leer Más” con Contexto

<!-- Usando aria-labelledby para contexto -->
<article class="card">
  <h3 id="article-1-title">10 Tips para Mejor Diseño UX</h3>
  <p>Mejora tus diseños con estos consejos prácticos...</p>
  <a href="/blog/tips-ux" aria-labelledby="article-1-title article-1-action">
    <span id="article-1-action">Leer artículo completo</span>
  </a>
</article>
<!-- Nombre accesible: "10 Tips para Mejor Diseño UX Leer artículo completo" -->

<!-- Usando aria-label para contexto -->
<article class="card">
  <h3>10 Tips para Mejor Diseño UX</h3>
  <p>Mejora tus diseños con estos consejos prácticos...</p>
  <a href="/blog/tips-ux" aria-label="Leer artículo completo: 10 Tips para Mejor Diseño UX">
    Leer más
  </a>
</article>

Patrones de Enlaces en Tablas

Enlaces en Celdas de Tabla

<table>
  <caption>Comparación de productos</caption>
  <thead>
    <tr>
      <th scope="col">Producto</th>
      <th scope="col">Precio</th>
      <th scope="col">Acciones</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row" id="product-a">Widget Pro</th>
      <td>$99</td>
      <td>
        <a href="/productos/widget-pro" aria-labelledby="product-a view-1">
          <span id="view-1">Ver detalles</span>
        </a>
      </td>
    </tr>
    <tr>
      <th scope="row" id="product-b">Widget Basic</th>
      <td>$49</td>
      <td>
        <a href="/productos/widget-basic" aria-labelledby="product-b view-2">
          <span id="view-2">Ver detalles</span>
        </a>
      </td>
    </tr>
  </tbody>
</table>

Enlaces de Acción en Tablas

<td class="actions-cell">
  <a href="/editar/123" aria-label="Editar Widget Pro">Editar</a>
  <a href="/eliminar/123" aria-label="Eliminar Widget Pro">Eliminar</a>
</td>

Implementaciones en Frameworks

Componente React

// AccessibleLink.tsx
import React from 'react';

interface AccessibleLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
  href: string;
  /** Si es true, abre en nueva pestaña con seguridad adecuada */
  external?: boolean;
  /** Texto oculto visualmente para agregar para lectores de pantalla */
  srAppend?: string;
  children: React.ReactNode;
}

export function AccessibleLink({
  href,
  external = false,
  srAppend,
  children,
  ...props
}: AccessibleLinkProps) {
  const externalProps = external ? {
    target: '_blank',
    rel: 'noopener noreferrer',
  } : {};

  return (
    <a href={href} {...externalProps} {...props}>
      {children}
      {srAppend && (
        <span className="visually-hidden">{srAppend}</span>
      )}
      {external && (
        <span className="visually-hidden">(abre en nueva pestaña)</span>
      )}
    </a>
  );
}

// Uso
<AccessibleLink href="/precios">
  Ver precios
</AccessibleLink>

<AccessibleLink
  href="/blog/articulo"
  srAppend="sobre Accesibilidad Web"
>
  Leer más
</AccessibleLink>

<AccessibleLink href="https://example.org" external>
  Visitar Example.org
</AccessibleLink>

Componente Vue 3

<!-- AccessibleLink.vue -->
<script setup lang="ts">
import { computed } from 'vue'

interface Props {
  href: string
  external?: boolean
  srAppend?: string
}

const props = withDefaults(defineProps<Props>(), {
  external: false
})

const linkProps = computed(() => {
  if (props.external) {
    return {
      target: '_blank',
      rel: 'noopener noreferrer'
    }
  }
  return {}
})
</script>

<template>
  <a :href="href" v-bind="linkProps">
    <slot />
    <span v-if="srAppend" class="visually-hidden">{{ srAppend }}</span>
    <span v-if="external" class="visually-hidden">(abre en nueva pestaña)</span>
  </a>
</template>

<style scoped>
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
</style>

Enlaces de Descarga y Documentos

Descargas de Archivos

<!-- Incluir tipo de archivo y tamaño -->
<a href="/archivos/informe-anual-2024.pdf">
  Descargar Informe Anual 2024 (PDF, 2.5MB)
</a>

<a href="/archivos/exportar-datos.xlsx">
  Descargar exportación de datos (Excel, 156KB)
</a>

<a href="/archivos/presentacion.pptx">
  Descargar diapositivas de presentación (PowerPoint, 5.2MB)
</a>

Enlaces Externos

<!-- Indicar destino externo -->
<a href="https://www.w3.org/WAI/" rel="external">
  W3C Web Accessibility Initiative
  <span class="visually-hidden">(enlace externo)</span>
</a>

<!-- O incluir en texto visible -->
<a href="https://www.w3.org/WAI/" rel="external">
  Visitar W3C Web Accessibility Initiative (sitio externo)
</a>

Enlaces de Nueva Pestaña

<!-- Siempre indicar comportamiento de nueva pestaña -->
<a href="/terminos" target="_blank" rel="noopener">
  Términos de Servicio
  <span class="visually-hidden">(abre en nueva pestaña)</span>
</a>

<!-- Indicación con icono con texto accesible -->
<a href="/privacidad" target="_blank" rel="noopener">
  Política de Privacidad
  <svg aria-hidden="true" class="external-icon"><!-- icono --></svg>
  <span class="visually-hidden">(abre en nueva pestaña)</span>
</a>

Probando Propósito del Enlace

Pruebas Automatizadas

// Test de Playwright para propósito del enlace
import { test, expect } from '@playwright/test';

test.describe('Cumplimiento de Propósito del Enlace', () => {
  test('sin texto de enlace genérico', async ({ page }) => {
    await page.goto('/');

    const genericPhrases = [
      'haga clic aquí',
      'aquí',
      'más',
      'leer más',
      'más información',
      'enlace',
      'esta página',
    ];

    const links = await page.locator('a').all();

    for (const link of links) {
      const text = await link.textContent();
      const ariaLabel = await link.getAttribute('aria-label');
      const accessibleName = ariaLabel || text?.trim().toLowerCase();

      // Verificar que el nombre accesible no es genérico
      for (const phrase of genericPhrases) {
        if (accessibleName === phrase) {
          const href = await link.getAttribute('href');
          throw new Error(
            `Texto de enlace genérico "${accessibleName}" encontrado para enlace a ${href}`
          );
        }
      }
    }
  });

  test('todos los enlaces tienen nombres accesibles', async ({ page }) => {
    await page.goto('/');

    const links = await page.locator('a').all();

    for (const link of links) {
      const text = await link.textContent();
      const ariaLabel = await link.getAttribute('aria-label');
      const ariaLabelledBy = await link.getAttribute('aria-labelledby');
      const title = await link.getAttribute('title');

      const hasAccessibleName =
        text?.trim() ||
        ariaLabel ||
        ariaLabelledBy ||
        title;

      if (!hasAccessibleName) {
        const href = await link.getAttribute('href');
        throw new Error(`Enlace a ${href} no tiene nombre accesible`);
      }
    }
  });
});

Checklist de Pruebas Manuales

  1. Navegación por Lista de Enlaces:

    • Usar lector de pantalla para listar todos los enlaces
    • Cada enlace debe tener sentido de forma aislada
    • Sin texto genérico duplicado
  2. Verificación de Contexto:

    • Para enlaces contextuales, verificar que el contexto está asociado programáticamente
    • Párrafo padre, elemento de lista o celda de tabla
  3. Verificación de Enlaces de Icono:

    • Navegar con Tab a cada enlace de icono
    • Verificar que el lector de pantalla anuncia el propósito
    • Indicador de foco visual presente

Errores Comunes y Soluciones

Error 1: Usar “Aquí” como Enlace

<!-- INCORRECTO -->
<p>Haga clic <a href="/registro">aquí</a> para registrarse.</p>

<!-- CORRECTO -->
<p><a href="/registro">Registrarse para una cuenta gratuita</a></p>

Error 2: “Leer Más” Duplicado

<!-- INCORRECTO -->
<a href="/articulo-1">Leer más</a>
<a href="/articulo-2">Leer más</a>

<!-- CORRECTO -->
<a href="/articulo-1" aria-label="Leer más sobre Título del Artículo 1">Leer más</a>
<a href="/articulo-2" aria-label="Leer más sobre Título del Artículo 2">Leer más</a>

Error 3: Enlace de Imagen Sin Alt

<!-- INCORRECTO -->
<a href="/inicio"><img src="logo.png"></a>

<!-- CORRECTO -->
<a href="/inicio"><img src="logo.png" alt="Nombre de Empresa - Ir a inicio"></a>

Checklist de Resumen

  • [ ] Todos los enlaces tienen nombres accesibles descriptivos
  • [ ] No hay “haga clic aquí,” “aquí,” o “leer más” sin contexto
  • [ ] Los enlaces solo con icono tienen aria-label o texto oculto visualmente
  • [ ] Los enlaces externos indican que abren en nuevo contexto
  • [ ] Las descargas de archivos incluyen formato y tamaño
  • [ ] Los patrones de tarjetas tienen estructura de enlaces accesible
  • [ ] Los enlaces de acción en tablas incluyen contexto de fila
  • [ ] Los textos de enlace duplicados tienen nombres accesibles únicos
  • [ ] Enlaces probados con navegación por lista de enlaces en lector de pantalla

Referencias

  1. W3C - WCAG 2.2 SC 2.4.4 Link Purpose (In Context)
  2. W3C - WCAG 2.2 SC 2.4.9 Link Purpose (Link Only)
  3. W3C - Technique H30: Providing link text
  4. W3C - Technique ARIA8: Using aria-label
  5. WebAIM - Links and Hypertext

Artículos relacionados

Versión relacionada

Introducción

Link Purpose Explained

El propósito del enlace describe hacia dónde lleva un enlace y qué sucede cuando los usuarios hacen clic en él

Hub de categoría

Hub

Wcag Compliance Hub

La accesibilidad web asegura que los sitios web y aplicaciones puedan ser utilizados por todos, incluyendo personas con discapacidades