Skip to main content
iOS / SwiftUI · WCAG 2.2 AA

Link (Standalone)

SwiftUI Link that opens a URL or web view, exposing the link trait (with the redundant button trait removed) so VoiceOver signals that activating it leaves the app.

Selection

The criteria an agent checks before retrieving this component.

Use when

  • Use when a standalone control opens a URL in the browser or an in-app web view, or otherwise leaves the current app context (e.g., "View Weekly Ad", "Open Help Center", "Privacy Policy"). Uses a native SwiftUI Link(destination:), whose link trait tells VoiceOver the control exits the app.
  • Use when the destination is a web address rather than an in-app screen.

Try a different component when

  • Do not use when the control triggers an immediate action and stays on the current screen (use button.basic).
  • Do not use when the control drills into another in-app screen (use list.row).
  • Do not use when the hyperlink sits inline within a run of paragraph text (use link.inline).

Must Haves

Non-negotiable structure. Every generated instance must satisfy these rules.

  • Use a native Link(destination:) so the control exposes the link trait and opens the URL through the system.
  • The link has an accessible name that describes its specific destination or purpose. When the link has visible text, the visible text serves as the accessible name.
  • Remove the redundant button trait when VoiceOver announces the link as "Button, Link": apply .accessibilityRemoveTraits(.isButton) so it announces only as a link. SwiftUI has historically added the button trait to Link alongside the link trait.
  • Link text meets 4.5:1 contrast against its background in both light and dark appearances. Use an accent or tint color vetted for both appearances (per global.semantic-color); the default link tint can fall below 4.5:1.
  • When the link sits inline with, or directly adjacent to, static text, distinguish it by more than color, e.g., .underline() on the label.
  • Meets the touch target size baseline in global_rules.md (global.touch-target-size).
  • Meets the system focus indicator baseline in global_rules.md (global.focus-visible).

Donts

Avoid these accessibility and UX barriers.

  • Do not implement a link as a Button that calls UIApplication.shared.open(url); VoiceOver then announces "Button" instead of "Link", hiding that the control leaves the app.
  • Do not use generic link text ("Click here", "here", "Read more", "Learn more") that does not describe the destination; the accessible name must be specific.
  • Do not rely on color alone to mark a link placed among static text; add an underline or other non-color distinction.
  • Do not leave the default button trait in place when it produces a "Button, Link" announcement.
  • Do not include the word "Link" in the accessible name; VoiceOver already appends the link trait.

Customizable

Alternatives and options that give the AI agent some room to move.

  • The accessible name is normally the visible link text. When the visible text cannot be made specific on its own, add an .accessibilityLabel naming the destination, with the visible text appearing at the start of the label.
  • The destination may open in the default browser or in an in-app web view (e.g., SFSafariViewController); the link trait and the labeling and contrast requirements are the same either way.
  • An .accessibilityHint may be added when the destination is not obvious from the visible text (e.g., "Opens in your browser."). Most links with specific text do not need one.

Golden Pattern

The tested reference implementation. Agents start from this shape and adapt to the developer’s codebase and context.

SWIFT
import SwiftUI

struct LinkBasicDemo: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
// Standalone link: specific text; button trait removed so VoiceOver says only "Link".
// The link tint must meet 4.5:1 in light and dark (set via AccentColor or .tint);
// the default tint can fall short.
Link("View Weekly Ad", destination: URL(string: "https://www.example.com/weekly-ad")!)
.accessibilityRemoveTraits(.isButton)

// Adjacent to static text: underline so it is distinct without color alone
HStack(spacing: 4) {
Text("Questions? Read our")
Link(destination: URL(string: "https://www.example.com/help")!) {
Text("Help Center")
.underline()
}
.accessibilityRemoveTraits(.isButton)
}

// Icon + text link still names its destination; the icon stays decorative
Link(destination: URL(string: "https://www.example.com/privacy")!) {
Label("Privacy Policy", systemImage: "lock.shield")
}
.accessibilityRemoveTraits(.isButton)
}
}
}

Acceptance Checks

The component’s test spec — an optional body of checks for verification.

Traits & semantics

  • Each link exposes the link trait and not the button trait; it is announced as a link, never "Button, Link" or "Button" alone.
  • The accessible name is the specific visible link text and does not contain the word "Link".

VoiceOver

  • Each link speaks a name that identifies its destination (e.g., "View Weekly Ad"), never generic text like "Click here".
  • Double-tapping a link opens its URL.

Switch Control & Full Keyboard Access

  • Every link is reachable and activatable via Switch Control and a hardware keyboard.

Dynamic Type

  • Link text scales with Dynamic Type and stays fully visible (no clipping or truncation) at accessibility text sizes.

Visual

  • Link text meets 4.5:1 contrast against its background in light and dark appearances.
  • A link placed inline with or adjacent to static text is distinguishable by more than color (e.g., underline).