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.
What Is Link Purpose?
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
2.4.4 Link Purpose (In Context) - Level A
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>
2.4.9 Link Purpose (Link Only) - Level AAA
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
Common Link Purpose Problems
1. Generic “Click Here” Links
<!-- 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>
2. Ambiguous “Read More” Links
<!-- 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>
3. URLs as Link Text
<!-- 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>
4. Missing Link Purpose
<!-- 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>
How to Write Good Link Text
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 |
Link Text Patterns
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.
Testing Link Purpose
Quick Manual Test
- List all links on the page
- Read each link text in isolation
- Can you determine where each link goes?
- 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
Accessible Name Computation for Links
How Links Get Their Names
The accessible name for a link is computed in this priority order:
- aria-labelledby - References other elements
- aria-label - Direct string label
- Link text content - Visible text and alt text of images
- 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>
Text Link Patterns
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.
Icon-Only Link Patterns
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>
Card Link Patterns
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>
Table Link Patterns
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
/>
Download and Document Links
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>
Testing Link Purpose
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
-
Link List Navigation:
- Use screen reader to list all links
- Each link should make sense in isolation
- No duplicate generic text
-
Context Verification:
- For contextual links, verify context is programmatically associated
- Parent paragraph, list item, or table cell
-
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
Related Articles
- ARIA Labels Guide - Accessible naming
- WCAG Compliance Hub - All accessibility evaluators
References
- W3C - WCAG 2.2 SC 2.4.4 Link Purpose (In Context)
- W3C - WCAG 2.2 SC 2.4.9 Link Purpose (Link Only)
- WebAIM - Links and Hypertext
- Deque University - Link Name Guidelines
- W3C - Technique H30: Providing link text
- W3C - Technique ARIA8: Using aria-label