UXR SEO Analyzer documentation

Introduction

Link Purpose

View contents

Introduction

Link purpose describes where a link leads and what happens when users click it. WCAG 2.4.4 (Link Purpose - In Context) requires that link purpose can be determined from the link text itself or from the surrounding context. This is a Level A requirement, making it fundamental to web accessibility.

For screen reader users who navigate by links, vague link text like “click here” or “read more” provides no useful information. When all links say the same thing, users cannot distinguish between them without navigating back to surrounding content.

The Basic Concept

Link purpose means the destination or action a link performs is clear to users:

<!-- Good: Purpose clear from link text -->
<a href="/pricing">View our pricing plans</a>

<!-- Poor: Purpose unclear without context -->
<a href="/pricing">Click here</a>

When links are clear, users can:

  • Quickly scan available navigation options
  • Decide whether to follow a link
  • Navigate efficiently using assistive technology
  • Understand links out of context (in link lists)

Who Benefits

User Type Benefit
Screen reader users Can navigate by links and understand destinations
Cognitive disabilities Clearer navigation reduces mental effort
Keyboard users Can make informed decisions about link navigation
All users Faster scanning and decision-making

WCAG Requirements

The purpose of each link can be determined from:

  • The link text alone, OR
  • The link text together with its programmatically determined link context

Programmatic context includes:

  • Text in the same sentence, paragraph, or list item
  • Text in the parent table cell or header
  • Text in ARIA-labelled regions
<!-- Purpose from link text alone -->
<a href="/report.pdf">Download Annual Report 2024 (PDF)</a>

<!-- Purpose from context -->
<p>
  Learn more about our services on the
  <a href="/services">services page</a>.
</p>

For enhanced accessibility, link purpose should be clear from the link text alone:

<!-- Level AAA: Self-descriptive links -->
<a href="/contact">Contact our sales team</a>
<a href="/docs">View API documentation</a>
<a href="/blog/seo-tips">Read: 10 SEO Tips for 2024</a>

Why Level A vs AAA?

Level A (2.4.4) accepts contextual links because:

  • Many design patterns rely on context (cards, tables)
  • Strict link-only requirements may be impractical
  • Context is still accessible to assistive technology

Level AAA (2.4.9) is stricter because:

  • Self-descriptive links work in any context
  • Better for link lists and navigation menus
  • Optimal user experience for all users
<!-- BAD: Generic, meaningless text -->
<p>To download the report, <a href="/report.pdf">click here</a>.</p>

<!-- GOOD: Descriptive link text -->
<p><a href="/report.pdf">Download the annual report (PDF, 2MB)</a></p>
<!-- BAD: Multiple identical links -->
<article>
  <h2>Product A</h2>
  <p>Description...</p>
  <a href="/products/a">Read more</a>
</article>
<article>
  <h2>Product B</h2>
  <p>Description...</p>
  <a href="/products/b">Read more</a>
</article>

<!-- GOOD: Unique, descriptive links -->
<article>
  <h2>Product A</h2>
  <p>Description...</p>
  <a href="/products/a">Learn more about Product A</a>
</article>
<!-- BAD: Raw URL as link text -->
<a href="https://example.com/path/to/page">https://example.com/path/to/page</a>

<!-- GOOD: Descriptive text with URL shown -->
<a href="https://example.com/path/to/page">Visit our resources page</a>
<!-- BAD: Link with no text -->
<a href="/search"><svg aria-hidden="true"><!-- icon --></svg></a>

<!-- GOOD: Accessible label provided -->
<a href="/search" aria-label="Search">
  <svg aria-hidden="true"><!-- icon --></svg>
</a>

Best Practices

Do Don’t
Describe the destination Use “click here” or “here”
Be specific and unique Use identical text for different links
Include file format if applicable Leave users guessing about downloads
Front-load important words Bury the meaning at the end
Keep links concise but clear Write entire paragraphs as links

Navigation Links:

<nav>
  <a href="/">Home</a>
  <a href="/about">About Us</a>
  <a href="/services">Our Services</a>
  <a href="/contact">Contact</a>
</nav>

Action Links:

<a href="/subscribe">Subscribe to newsletter</a>
<a href="/demo">Request a demo</a>
<a href="/download">Download free trial</a>

Document Links:

<a href="/annual-report-2024.pdf">
  Annual Report 2024 (PDF, 2.5MB)
</a>

External Links:

<a href="https://example.org" rel="external">
  Visit Example Organization (opens external site)
</a>

Using aria-label for Context

When visual design requires short link text, use aria-label for screen readers:

<!-- Card design with "Read more" visible -->
<article>
  <h3>Understanding Accessibility</h3>
  <p>Accessibility ensures everyone can use your website...</p>
  <a href="/blog/accessibility" aria-label="Read more about Understanding Accessibility">
    Read more
  </a>
</article>

Important: aria-label completely replaces the visible link text for screen readers, so it must be complete and descriptive.

Quick Manual Test

  1. List all links on the page
  2. Read each link text in isolation
  3. Can you determine where each link goes?
  4. Are any links ambiguous or identical?

What to Verify

  • [ ] Every link has descriptive text or accessible name
  • [ ] No “click here” or “here” links
  • [ ] “Read more” links have unique accessible labels
  • [ ] Icon-only links have aria-label or visually hidden text
  • [ ] Link purpose is clear from text or immediate context

Tools for Testing

  • Screen reader - Navigate by links only
  • axe DevTools - Detects empty or generic links
  • WAVE - Highlights link text issues
  • Keyboard - Tab through links and verify purpose

Implementation

How Links Get Their Names

The accessible name for a link is computed in this priority order:

  1. aria-labelledby - References other elements
  2. aria-label - Direct string label
  3. Link text content - Visible text and alt text of images
  4. title attribute - Last resort (not recommended)
<!-- Priority 1: aria-labelledby -->
<span id="product-name">Product A</span>
<a href="/products/a" aria-labelledby="product-name learn-more-1">
  <span id="learn-more-1">Learn more</span>
</a>
<!-- Accessible name: "Product A Learn more" -->

<!-- Priority 2: aria-label -->
<a href="/products/a" aria-label="Learn more about Product A">
  Learn more
</a>
<!-- Accessible name: "Learn more about Product A" -->

<!-- Priority 3: Link text content -->
<a href="/products/a">View Product A details</a>
<!-- Accessible name: "View Product A details" -->

Best Practice: Use Visible Text When Possible

<!-- BEST: Visible text is descriptive -->
<a href="/pricing">View our pricing plans</a>

<!-- ACCEPTABLE: aria-label supplements short visible text -->
<a href="/pricing" aria-label="View pricing plans">
  Pricing
</a>

<!-- AVOID: title attribute for accessible name -->
<a href="/pricing" title="View pricing plans">
  Pricing
</a>

Self-Descriptive Links (Level AAA)

<!-- Navigation links -->
<nav aria-label="Main navigation">
  <a href="/">Home</a>
  <a href="/about">About our company</a>
  <a href="/services">Our services</a>
  <a href="/portfolio">View our portfolio</a>
  <a href="/contact">Contact us</a>
</nav>

<!-- Action links -->
<a href="/signup">Create your free account</a>
<a href="/demo">Request a product demo</a>
<a href="/subscribe">Subscribe to our newsletter</a>

<!-- Content links -->
<a href="/blog/accessibility-guide">
  Read: The Complete Guide to Web Accessibility
</a>

Contextual Links (Level A)

When context clarifies purpose, simpler link text is acceptable:

<!-- Paragraph context -->
<p>
  We offer three service tiers to meet your needs. Visit our
  <a href="/pricing">pricing page</a>
  to compare options.
</p>

<!-- List context -->
<ul>
  <li>
    <strong>Accessibility Audit</strong> - Comprehensive review of your site.
    <a href="/services/audit">Learn more</a>
  </li>
  <li>
    <strong>UX Research</strong> - User testing and analysis.
    <a href="/services/research">Learn more</a>
  </li>
</ul>

Note: While contextual links pass Level A, making all links self-descriptive (Level AAA) provides a better user experience.

Basic Icon Link

<!-- Using aria-label -->
<a href="/search" class="icon-link" aria-label="Search">
  <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>

<!-- Using visually hidden text -->
<a href="/search" class="icon-link">
  <span class="visually-hidden">Search</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>

Social Media Links

<nav aria-label="Social media">
  <a href="https://twitter.com/company" aria-label="Follow us on Twitter">
    <svg aria-hidden="true"><!-- Twitter icon --></svg>
  </a>
  <a href="https://linkedin.com/company/name" aria-label="Connect on LinkedIn">
    <svg aria-hidden="true"><!-- LinkedIn icon --></svg>
  </a>
  <a href="https://github.com/company" aria-label="View our GitHub">
    <svg aria-hidden="true"><!-- GitHub icon --></svg>
  </a>
</nav>

Action Icon Links

<div class="actions">
  <a href="/edit/123" aria-label="Edit document">
    <svg aria-hidden="true"><!-- edit icon --></svg>
  </a>
  <a href="/delete/123" aria-label="Delete document">
    <svg aria-hidden="true"><!-- delete icon --></svg>
  </a>
  <a href="/download/123" aria-label="Download document">
    <svg aria-hidden="true"><!-- download icon --></svg>
  </a>
</div>

Entire Card as Link

<!-- Simple card link -->
<a href="/blog/article-slug" class="card-link">
  <article class="card">
    <img src="/images/article.jpg" alt="">
    <h3>Understanding Web Accessibility</h3>
    <p>Learn the fundamentals of creating accessible websites...</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>

Card with Multiple Links

<article class="card">
  <img src="/images/article.jpg" alt="">
  <h3>
    <a href="/blog/accessibility" class="card-title-link">
      Understanding Web Accessibility
    </a>
  </h3>
  <p>Learn the fundamentals of creating accessible websites...</p>
  <div class="card-meta">
    <a href="/category/accessibility" class="category-link">
      Accessibility
    </a>
    <a href="/author/jane-doe" class="author-link">
      By Jane Doe
    </a>
  </div>
</article>

<style>
/* Make title link cover entire card for easy clicking */
.card {
  position: relative;
}

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

/* Raise other links above the overlay */
.category-link,
.author-link {
  position: relative;
  z-index: 1;
}
</style>

“Read More” Pattern with Context

<!-- Using aria-labelledby for context -->
<article class="card">
  <h3 id="article-1-title">10 Tips for Better UX Design</h3>
  <p>Improve your designs with these practical tips...</p>
  <a href="/blog/ux-tips" aria-labelledby="article-1-title article-1-action">
    <span id="article-1-action">Read full article</span>
  </a>
</article>
<!-- Accessible name: "10 Tips for Better UX Design Read full article" -->

<!-- Using aria-label for context -->
<article class="card">
  <h3>10 Tips for Better UX Design</h3>
  <p>Improve your designs with these practical tips...</p>
  <a href="/blog/ux-tips" aria-label="Read full article: 10 Tips for Better UX Design">
    Read more
  </a>
</article>

Links in Table Cells

<table>
  <caption>Product comparison</caption>
  <thead>
    <tr>
      <th scope="col">Product</th>
      <th scope="col">Price</th>
      <th scope="col">Actions</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row" id="product-a">Widget Pro</th>
      <td>$99</td>
      <td>
        <a href="/products/widget-pro" aria-labelledby="product-a view-1">
          <span id="view-1">View details</span>
        </a>
      </td>
    </tr>
    <tr>
      <th scope="row" id="product-b">Widget Basic</th>
      <td>$49</td>
      <td>
        <a href="/products/widget-basic" aria-labelledby="product-b view-2">
          <span id="view-2">View details</span>
        </a>
      </td>
    </tr>
  </tbody>
</table>

Action Links in Tables

<td class="actions-cell">
  <a href="/edit/123" aria-label="Edit Widget Pro">Edit</a>
  <a href="/delete/123" aria-label="Delete Widget Pro">Delete</a>
</td>

Framework Implementations

React Link Component

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

interface AccessibleLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
  href: string;
  /** If true, opens in new tab with proper security */
  external?: boolean;
  /** Visually hidden text to append for screen readers */
  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">(opens in new tab)</span>
      )}
    </a>
  );
}

// Usage
<AccessibleLink href="/pricing">
  View pricing
</AccessibleLink>

<AccessibleLink
  href="/blog/article"
  srAppend="about Web Accessibility"
>
  Read more
</AccessibleLink>

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

Vue 3 Link Component

<!-- 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">(opens in new tab)</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>

Icon Link Component

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

interface IconLinkProps {
  href: string;
  label: string;
  icon: React.ReactNode;
  external?: boolean;
}

export function IconLink({ href, label, icon, external }: IconLinkProps) {
  return (
    <a
      href={href}
      className="icon-link"
      aria-label={label}
      {...(external && { target: '_blank', rel: 'noopener noreferrer' })}
    >
      {icon}
    </a>
  );
}

// Usage
<IconLink
  href="/search"
  label="Search"
  icon={<SearchIcon aria-hidden="true" />}
/>

<IconLink
  href="https://twitter.com/company"
  label="Follow us on Twitter"
  icon={<TwitterIcon aria-hidden="true" />}
  external
/>

File Downloads

<!-- Include file type and size -->
<a href="/files/annual-report-2024.pdf">
  Download Annual Report 2024 (PDF, 2.5MB)
</a>

<a href="/files/data-export.xlsx">
  Download data export (Excel, 156KB)
</a>

<a href="/files/presentation.pptx">
  Download presentation slides (PowerPoint, 5.2MB)
</a>

External Links

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

<!-- Or include in visible text -->
<a href="https://www.w3.org/WAI/" rel="external">
  Visit W3C Web Accessibility Initiative (external site)
</a>

New Tab Links

<!-- Always indicate new tab behavior -->
<a href="/terms" target="_blank" rel="noopener">
  Terms of Service
  <span class="visually-hidden">(opens in new tab)</span>
</a>

<!-- Icon indication with accessible text -->
<a href="/privacy" target="_blank" rel="noopener">
  Privacy Policy
  <svg aria-hidden="true" class="external-icon"><!-- icon --></svg>
  <span class="visually-hidden">(opens in new tab)</span>
</a>

Automated Testing

// Playwright test for link purpose
import { test, expect } from '@playwright/test';

test.describe('Link Purpose Compliance', () => {
  test('no generic link text', async ({ page }) => {
    await page.goto('/');

    const genericPhrases = [
      'click here',
      'here',
      'more',
      'read more',
      'learn more',
      'link',
      'this page',
    ];

    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();

      // Check accessible name isn't generic
      for (const phrase of genericPhrases) {
        if (accessibleName === phrase) {
          const href = await link.getAttribute('href');
          throw new Error(
            `Generic link text "${accessibleName}" found for link to ${href}`
          );
        }
      }
    }
  });

  test('all links have accessible names', 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(`Link to ${href} has no accessible name`);
      }
    }
  });

  test('icon links have accessible labels', async ({ page }) => {
    await page.goto('/');

    // Find links that only contain SVG or images
    const iconLinks = await page.locator('a:has(svg):not(:has-text(""))').all();

    for (const link of iconLinks) {
      const ariaLabel = await link.getAttribute('aria-label');
      const hiddenText = await link.locator('.visually-hidden').textContent();

      if (!ariaLabel && !hiddenText) {
        const href = await link.getAttribute('href');
        throw new Error(`Icon link to ${href} has no accessible label`);
      }
    }
  });
});

Manual Testing Checklist

  1. Link List Navigation:

    • Use screen reader to list all links
    • Each link should make sense in isolation
    • No duplicate generic text
  2. Context Verification:

    • For contextual links, verify context is programmatically associated
    • Parent paragraph, list item, or table cell
  3. Icon Link Verification:

    • Tab to each icon link
    • Verify screen reader announces purpose
    • Visual focus indicator present

Common Mistakes and Fixes

Mistake 1: Using “Here” as Link

<!-- WRONG -->
<p>Click <a href="/signup">here</a> to sign up.</p>

<!-- CORRECT -->
<p><a href="/signup">Sign up for a free account</a></p>

Mistake 2: Duplicate “Read More”

<!-- WRONG -->
<a href="/article-1">Read more</a>
<a href="/article-2">Read more</a>

<!-- CORRECT -->
<a href="/article-1" aria-label="Read more about Article Title 1">Read more</a>
<a href="/article-2" aria-label="Read more about Article Title 2">Read more</a>

Mistake 3: Image Link Without Alt

<!-- WRONG -->
<a href="/home"><img src="logo.png"></a>

<!-- CORRECT -->
<a href="/home"><img src="logo.png" alt="Company Name - Go to homepage"></a>

Summary Checklist

  • [ ] All links have descriptive accessible names
  • [ ] No “click here,” “here,” or “read more” without context
  • [ ] Icon-only links have aria-label or visually hidden text
  • [ ] External links indicate they open in new context
  • [ ] File downloads include format and size
  • [ ] Card patterns have accessible link structure
  • [ ] Table action links include row context
  • [ ] Duplicate link texts have unique accessible names
  • [ ] Links tested with screen reader link list navigation

References

  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. WebAIM - Links and Hypertext
  4. Deque University - Link Name Guidelines
  5. W3C - Technique H30: Providing link text
  6. W3C - Technique ARIA8: Using aria-label

Related articles

Category hub

Hub

WCAG 2.2 Compliance Hub

Web accessibility ensures that websites and applications can be used by everyone, including people with disabilities

Last updated: