Bridge view
Link
An interactive element that navigates the user to another resource — another page, an anchor within the current page, an external URL, a downloadable file, or a `mailto:` / `tel:` target. Distinct from Button (which performs an action without navigation). Renders as `<a href>` in HTML and inherits the entire native anchor contract: middle-click opens in a new tab, right-click exposes the URL, copy- as-link works, the URL is visible in the status bar.
When to use
Use
When the user needs to navigate to another resource — another page, an anchor on the current page, an external URL, a downloadable file, or a `mailto:` / `tel:` target. Default activator for navigation.
Avoid
For in-page actions that do not navigate (form submit, modal open, panel toggle) — that is `Button`. For the disclosure affordance of a menu — that is `MenuButton`. For decorative in-prose markers that should not be interactive — that is plain text (or a `<dfn>` / `<abbr>` element).
Versus related
- button
`Link` navigates; `Button` performs an action without navigation. Visual styling may converge (button-styled link, link-styled button), but the semantic choice never does — middle-click, copy-link, status-bar URL preview, and keyboard activation (Enter-only vs Enter+Space) all depend on the underlying element.
- menu-button
`MenuButton` opens a menu of options and exposes `aria-haspopup="menu"`. `Link` navigates immediately on activation. A "links-list rendered as a menu" pattern is either a real menu (MenuButton + menu items) or a navigation list with `<a>` items — never both.
Figma↔Code mismatches
Where designer and developer worlds typically misalign on this component.
- 01 Figma
An underlined text style drawn for inline links in body prose
CodeA `<a>` element with `text-decoration: underline` plus `:hover` reinforcement and `:focus-visible` ring
ConsequenceDesigners may rely on Figma's underline rendering and forget that `text-decoration` interacts with line-height and descender clearance differently per browser. Removing the underline entirely (relying on color alone) violates WCAG 1.4.1 (Use of Color); Figma can show a styled link without an underline and pass a visual review the rendered version would fail.
CorrectDocument underline as the canonical default for inline links; colour-only differentiation is the documented WCAG failure case to avoid. Standalone and button-styled variants may opt out of underline only when they carry sufficient non-color affordances (button-styled buttons, distinct background).
- 02 Figma
A "link styled as button" variant drawn alongside actual buttons
CodeA `<a class="button">` with button visual treatment but anchor semantics
ConsequenceDesigners and developers diverge on whether the element is a Link or a Button — they look identical visually but their keyboard contracts differ (Enter only on `<a>`, Enter + Space on `<button>`), middle-click behavior differs, and form participation differs. The Figma file does not encode the semantic choice.
CorrectTreat the semantic question (does this navigate, or does it perform an in-page action?) as the canonical distinguisher. A "link styled as a button" remains a Link with anchor semantics; a button-styled `<a>` does not get Spacebar activation. Document the variant explicitly as `variant: button-styled` so the visual treatment is named and the semantic choice stays clear.
- 03 Figma
External link icon drawn as a separate static glyph next to the label
CodeA `::after` pseudo-element on `[target="_blank"]` or a slot-based icon driven by the `external` property
ConsequenceDesigners may forget the "external" indicator on individual links and developers may forget to set `rel="noopener noreferrer"`. The two artefacts disagree about which links go external; users cannot predict before clicking which links will open a new tab.
CorrectModel `external` as a boolean property. The Figma component exposes it as a Boolean; the code wires it to both the visual icon and the `target="_blank" rel="noopener noreferrer"` attributes plus an `aria-label` suffix or hidden text announcing "(opens in new tab)".
- 04 Figma
Disabled link drawn as greyed-out text with no underline
CodeThere is no `disabled` attribute on `<a>` — `aria-disabled="true"` plus `tabindex="-1"` plus removed `href` is the closest equivalent
ConsequenceDesigners may treat "disabled link" as a real state; developers shipping it implement disabled inconsistently — sometimes the `<a>` is still keyboard-reachable but a no-op (confusing), sometimes it's removed from focus order entirely (announced as nothing).
CorrectDocument that "disabled link" is not a native concept. Either replace the `<a>` with a span (no semantic), or use `aria-disabled="true"` plus an `onClick` guard plus removal of `href` (announces as a non-focusable disabled link in some SR). Prefer not rendering the link at all when the destination is genuinely unavailable.
Variants, properties, states
Variants
Structurally different versions of the component.
inlinestandalonebutton-styled Properties
The same component, parameterised.
| Property | Type |
|---|---|
external | boolean |
download | boolean |
size | sm | md | lg |
emphasis | subtle | default | strong |
States
Browser/user-driven (interactive) vs. app-driven (data).
| Kind | States |
|---|---|
interactive | hoverfocus-visibleactivevisited |
data | disabledloading |
Figma ↔ Code property map
| Figma | Type | Code | Notes |
|---|---|---|---|
Variant | Variant | variant | Maps inline / standalone / button-styled. |
Size | Variant | size | sm / md / lg. Mostly relevant for standalone variant; inline links inherit surrounding text size. |
Emphasis | Variant | emphasis | subtle / default / strong. Drives colour intensity and underline thickness. |
External | Boolean | external | Toggles trailing-icon visibility and adds `target="_blank" rel="noopener noreferrer"` plus a "(opens in new tab)" SR announcement. |
Download | Boolean | download | Toggles trailing-icon visibility and sets the `download` attribute. Cross-origin caveat documented in mistakes. |
Has Trailing Icon | Boolean | iconTrailing | Slot-visibility toggle; auto-true when external or download is true. |
Trailing Icon | Instance Swap | iconTrailing | Swap the trailing-icon component (chevron, arrow, download-arrow, external-square). |
Label | Text | children | Default slot — the link text. |
Visited | Boolean | data-visited | Figma-only state for design preview; in production the `:visited` pseudo-class drives the visual without a code prop. |
Figma anatomy
| Slot | Figma type | Hint |
|---|---|---|
root | text | Inline text or auto-layout horizontal frame; bound to "destination" property documented separately |
label | text | Inline text style; underline applied by variant treatment |
icon-trailing | instance | Icon component instance; visibility bound to the relevant property |
visited-marker | rectangle | 6×6 dot or label-color-shift; visibility bound to "visited" state |
Code anatomy
| Slot | Code slot | Semantic |
|---|---|---|
root | root | anchor |
label | label | text |
icon-trailing | icon-trailing | presentational-or-img |
visited-marker | visited | presentational |
Cross-framework expression
| Framework | Structure mechanism | Variant mechanism |
|---|---|---|
| Web Components | A `<ui-link>` host element internally rendering a real `<a>` with `href` reflected from a `to` attribute; named slots for icon-trailing | attributes (`variant="standalone"`, `external`, `download`, `size="md"`); reflected to host classes |
| React | a single `<Link>` component that wraps `<a>` (or framework router's `<Link>` like Next.js, React Router); accepts `to` plus icon-trailing children | props with class-variance-authority; `external` and `download` boolean props drive both attributes and visual indicator |
| Angular (signals) | a `[uiLink]` directive on `<a>` plus optional `<ui-link>` component that wraps internal `<a>`; supports Angular Router `[routerLink]` for in-app navigation | input<'inline' | 'standalone' | 'button-styled'>(); `[external]`, `[download]` host bindings reflect to attributes |
| Vue | a `<Link>` SFC wrapping `<a>` (or `<router-link>` for Vue Router); slot for icon-trailing | defineProps with literal-union types; `:external` boolean drives the indicator and rel attribute |
Internationalisation
RTL · mirroring
Underline and text alignment follow the document direction (LTR underline below the baseline; RTL same — underlines are direction-neutral). Trailing icons (external arrow, download arrow) move from inline-end to inline-start visually but remain semantically "trailing"; their glyphs do *not* mirror (a download arrow points down in both directions; an external-square glyph is direction-neutral). Numerals in link text follow the locale's preferred numeral system. Chevrons indicating jump direction (anchor-down, see-also-right) flip horizontally to preserve semantic direction.
Text expansion
Link labels can grow significantly. Inline links inside prose inherit line-wrap; standalone links may need a larger inline-size budget. Avoid hard-coding link min-width; use the label's natural width plus padding (button-styled variant only). Generic phrases like "Read more" / "Learn more" expand differently in DE / RU / FI — DE "Mehr erfahren" is shorter than "Read more"; RU "Подробнее" is longer; the canonical surface is sized for the longest expected expansion.
Accessibility
| Slot | Accessibility hint | |
|---|---|---|
root | Always render as `<a href="...">`. A link without `href` has no role, is not focusable, and is not announced as a link by SR. Avoid `<div>` or `<span>` with click handlers and `role="link"` — the native element is always preferable. Visited state must be perceivable (CSS `:visited` styled beyond the page baseline) for content where revisit recall matters. | |
label | Plain text node. Avoid generic phrases ("read more", "click here") — they are uninformative when SR users navigate by link list. Distinguish links to the same destination from links to different destinations through the link text alone, not just through surrounding context. | |
icon-trailing | Decorative when its meaning is also conveyed in the label text or the link's accessible name (e.g. "Download report, opens PDF" with a download icon). When the icon carries meaning the label omits, surface the meaning in `aria-label` on the link, not on the icon. Per WCAG, "opens in new tab" / "external link" must be announced for SR users. | |
visited-marker | Visited state is rendered visually only. SR does not announce "visited" by default. For lists where revisit recall matters (search results, archives), pair the visual marker with an `aria-describedby` reference to a visually-hidden "visited" text — but only on links the user is meant to revisit-aware. |
Accessibility acceptance
Keyboard walk
| Keys | Expected |
|---|---|
Tab | Focus enters the link in document order. Disabled-styled links (via aria-disabled) remain focusable so SR users hear the disabled-link state; truly unavailable links should be rendered as plain text instead. |
Enter | Activates the link — navigates to the `href`. Spacebar does not activate (anchors are Enter-only by native convention, unlike buttons). |
Shift+Click or middle-click (when the underlying device supports) | Opens the link in a new tab. Native `<a>` honours this for free; bespoke `role="link"` divs do not. |
Screen-reader announcements
| Trigger | Expected |
|---|---|
| Focus enters an internal link | SR announces the link's accessible name followed by "link" (e.g. "Q3 report, link"). The destination URL is *not* spoken automatically; verbose verbosity modes may include it. |
| Focus enters an external link with `external: true` | SR announces "<label>, link, opens in new tab" (or equivalent — "external link"). The "opens in new tab" portion comes from the canonical announcement pattern (visually-hidden suffix or `aria-label` extension). |
| Focus enters a download link with `download: true` | SR announces the link followed by file metadata: "<label> (PDF, 2 MB), link". The metadata lives in the link text or an accessible-name suffix; SR does not infer it from the file extension. |
| Visited state set | Visited is rendered visually only; SR does not announce "visited" by default. For revisit-aware lists, the accessible name may include a "visited" suffix via `aria-describedby`. |
axe-core rules to assert
link-namelink-in-text-blockcolor-contrasttarget-sizeidentical-links-same-purpose
Common mistakes
#link-clickable-div
Clickable `<div>` styled as a link
A `<div onclick="...">` styled with link colour and underline. Has no role, is not focusable, is not announced as a link by SR, does not support middle-click "open in new tab" or right-click "copy link", does not support keyboard activation without bespoke handlers.
Always use `<a href="...">` for navigation. The native element is the canonical choice; `role="link"` on a div is a last-resort ARIA fallback that still loses native browser affordances (middle-click, copy-link, status-bar URL preview).
#link-as-button
`<a>` used for an in-page action
A link wired with `onclick` performing an in-page action (toggling a panel, submitting a form). Middle-click opens an empty page; the URL is visible in the status bar but leads nowhere meaningful; `href="#"` causes scroll-to-top on JS failure.
Use `<button>` for in-page actions. The semantic distinction ("link navigates, button performs") is the right test. If the visual treatment requires a link's appearance, use a Button with the `tertiary` or `ghost` variant — not an `<a>`.
#link-uninformative-text
Link text is "click here" or "read more"
SR users navigating by link list (a common SR navigation pattern) hear "click here" / "read more" detached from surrounding context. The link is unidentifiable.
Link text describes the destination meaningfully out of context: "Read the Q3 report" instead of "click here". When the visual design demands short link text adjacent to descriptive prose, use `aria-labelledby` to assemble the full accessible name from both the prose and the link text.
#link-external-no-rel-or-indication
External link without `rel` or visual indicator
`target="_blank"` without `rel="noopener noreferrer"` exposes the parent window via `window.opener` (security and tabnabbing risk). Without a visual indicator the user cannot predict that the link opens a new tab — back-button behaviour is broken from their perspective.
`target="_blank"` always pairs with `rel="noopener noreferrer"`. Render an external-indicator icon, append "(opens in new tab)" to the accessible name (visible or visually-hidden), and announce it on focus via `aria-label` or a hidden `<span>`.
#link-download-no-fallback
Download link without metadata or fallback for failed downloads
`<a href="report.pdf" download>` does not announce file format or size, and the `download` attribute is silently ignored on cross-origin URLs (the file opens inline instead). Users on restricted environments may lose the file.
Pair the download link with file-type and size metadata in visible text or an accessible-name suffix: "Download Q3 report (PDF, 2 MB)". Document the cross-origin caveat: download links to other origins require server-side `Content-Disposition: attachment` to enforce download.
#link-visited-color-only
Visited state distinguished by color alone
The link's `:visited` colour is the only differentiator from `:link`. Users with colour-vision deficiencies or in high ambient light cannot tell visited from unvisited.
Pair colour with a non-color visited cue — a small dot, a checkmark, or a slightly different weight or underline style. The CSS `:visited` selector is privacy-restricted (limited properties); the visited marker slot in the canonical anatomy handles non-restricted properties.