# CTA Link
Navigate users to another page or URL with a prominent, action-oriented link.
Section: components
Docs: https://nebula.kaluza.com/components/cta-link
---
## When to use
- To navigate the user to a different page or URL as a clear, prominent call to action
- When the destination is the next step in a journey, such as "View account" or "See full details"
- When a high-visibility link is needed and the action is navigating rather than triggering something on the current page
## When not to use
- **To trigger an action on the current page** - use [CTA Button](/components/cta-button) instead. Buttons act; links navigate. Using a link element for a same-page action breaks the semantic contract of HTML and confuses assistive technology.
- **For inline navigation within a sentence or paragraph** - use [Text Link](/components/text-link) instead. CTA Link is a standalone control and disrupts reading flow when placed inside body text.
## Anatomy
1. **Label**
2. **Right icon** (default: chevron-right; can be suppressed or replaced)
3. **Left icon** (optional)
## Basic usage
```tsx
Plant a tree
```
## Variants and features
### Primary
Use for the main navigational call to action on a page or in a section. There should be at most one primary CTA Link per section.
```tsx
Plant a tree
```
### Secondary
Use for supporting navigational actions that sit alongside a primary button or link, such as "View details" next to "Get started".
```tsx
Find out more about trees
```
### Tertiary
Use for low-emphasis navigational actions that are available but not the focus of the page.
```tsx
See all options
```
### Destructive secondary
Use when the navigation leads to a page where a destructive or irreversible action will be performed, such as a dedicated cancellation or deletion confirmation page. The outlined red styling signals risk without triggering the action immediately.
Note: a filled destructive variant (filled red background) is not available on CTA Link. If you need a filled destructive style, the action belongs on that destination page as a [CTA Button](/components/cta-button).
```tsx
Cancel my green plan
```
### Destructive tertiary
Use for low-emphasis destructive navigation, when the risk needs to be communicated but the action should not be visually dominant.
```tsx
Remove saved address
```
### Default chevron icon
CTA Link includes a `chevron-right` icon on the right by default. This indicates that the user will be taken somewhere, and there is more to see or do at the destination.
To suppress the chevron when the destination is already evident from context (for example, when the link label names the destination precisely and an icon would be redundant), pass `iconRight="off"` explicitly.
```tsx
View account details
```
### Opens in new window
Set `opensInNewWindow` to `true` when the link must open in a new browser tab. This sets `target="_blank"`, replaces the default chevron with a new-window icon, and adds visually hidden text reading " (opens in new window)" so screen reader users are informed before activating the link.
Do not set `target="_blank"` directly; use the `opensInNewWindow` prop so that all related behaviour stays consistent.
```tsx
More facts about trees
```
### Using React Router
By default, CTA Link renders as an `` element. To use React Router's `Link` component for client-side navigation in a single-page application, pass it via the `forwardedAs` prop. Don't use `as` for this: it would replace CTA Link itself with `Link` and lose its icons. All other props behave the same.
This example is not live, because the docs site does not use React Router.
```tsx
import { Link } from 'react-router-dom';
View account
```
### Icons
An icon can be added to the left of the label with `iconLeft`. Use a left icon when it reinforces what the link leads to, for example a download icon before "Download statement".
```tsx
Download statement
```
The right icon can be replaced with any icon name. Pass the icon name to `iconRight` to override the default chevron.
```tsx
Download statement
```
### Small size
Use `size="small"` for compact contexts such as table rows or dense UI where the default link height would be too large.
```tsx
View account details
```
### Full width
The `fullWidth` prop controls how the link sizes to its container:
- `'small'` (default): full width below the smallest breakpoint, auto width above
- `'always'`: full width at all breakpoints
- `'never'`: always auto width
```tsx
Plant a tree
```
## Content guidance
Labels must describe the destination or action. "Click here", "Find out more", and similar vague phrases must not be used. Screen reader users navigate by links alone and hear labels out of context, so a vague label provides no information about where the link goes.
- Use sentence case: "View account details", not "View Account Details"
- Start with a verb or noun that describes the destination: "View", "Download", "Go to", "See"
- Each CTA Link on a page should have a unique label. If two links go to different places but have the same label, add visually hidden text to distinguish them
- Avoid possessive pronouns: "View account details" rather than "View your account details"
- When using `opensInNewWindow`, write labels that are still meaningful without the "(opens in new window)" suffix
Common patterns:
| Context | Label |
|---|---|
| Viewing more detail | "View account details", "See full breakdown" |
| Continuing a journey | "Continue", "Next step" |
| External reference | "Energy price cap information", "Ofgem guidance" |
| Downloading a file | "Download statement", "Get invoice" |
## Rules and guidance
### Usage
- **CTALink** MUST be used only for navigation to a URL, not to trigger same-page actions. Rationale: CTALink renders as an element. Anchor elements navigate; button elements act. Using a link for a same-page action breaks the HTML semantic contract, confuses assistive technology, and breaks standard browser behaviour (middle-click, open in new tab). [id: ctalink.navigation-only; testability: auto]
- **CTALink** MUST receive a valid href (or a to prop when used with a router component). Rationale: An anchor without a href is not a link. It is not keyboard-focusable and provides no navigation target. [id: ctalink.has-href; testability: auto]
- **CTALink** MUST NOT use the 'destructive' variant (filled red background). Rationale: Only 'destructive-secondary' (outlined red) is available on CTALink. The filled destructive style is reserved for CTAButton, which confirms an action. A link leading to a destructive confirmation page uses the outlined style to signal risk without triggering anything immediately. [id: ctalink.destructive-secondary-only; testability: auto]
- **CTALink** MUST NOT use the loading prop. Rationale: CTALink does not wrap its content in LoadingContent. The loading prop has no effect and will not produce a spinner. Use CTAButton if a loading state is required. [id: ctalink.no-loading-prop; testability: auto]
- **CTALink** SHOULD use the as prop with a router Link component for client-side navigation in single-page applications. Rationale: Using a plain href for SPA navigation causes a full page reload. The as prop allows CTALink to use the router's Link while preserving all visual and accessibility behaviour. [id: ctalink.router-link; testability: manual]
- **CTALink** SHOULD NOT suppress the default chevron-right icon without a clear contextual reason. Rationale: The chevron signals that the user will be taken to another page and there is more to see at the destination. Removing it makes the link look like a button and reduces scannability. Suppress it (iconRight="off") only when the destination is already completely evident from context. [id: ctalink.default-chevron; testability: manual]
### Placement
- **PrimaryCTALink** SHOULD NOT appear more than once in a single section or card. Rationale: Multiple primary actions compete for attention and dilute the hierarchy. [id: ctalink.one-primary-per-section; testability: manual]
### Content
- **CTALink** MUST have a label that is unique on the page, or include visually hidden text to distinguish it from other links. Rationale: Screen reader users navigate by links and hear labels out of context. Multiple links with the same label (e.g. two "View details" links) are indistinguishable without surrounding context. [id: ctalink.unique-label; testability: auto]
- **CTALink** MUST NOT use labels such as "Click here", "Here", "Read more", or "Learn more" without additional context. Rationale: These labels are meaningless when heard out of context by a screen reader user. [id: ctalink.no-click-here; testability: manual]
- **CTALink** SHOULD use sentence case for the label. [id: ctalink.sentence-case; testability: auto]
- **CTALink** SHOULD start the label with a verb or a noun that describes the destination. Rationale: Labels like "Click here" or "Read more" fail users who navigate by links out of context. Specific labels like "View account details" or "Download statement" convey the destination before activation. [id: ctalink.active-language; testability: manual]
- **CTALink** SHOULD use iconLeft only when the icon reinforces what the link leads to. Rationale: Decorative icons add visual noise without aiding comprehension. [id: ctalink.icon-meaning; testability: manual]
### Interaction
- **CTALink** WILL default iconRight to "chevron-right", or "new-window" when opensInNewWindow is true, unless iconRight is explicitly set. [id: ctalink.will-default-icon-right; testability: auto]
- If opensInNewWindow is true: **CTALink** WILL add a visually hidden "(opens in new window)" disclosure and a new-window icon when opensInNewWindow is true. Rationale: The opensInNewWindow prop adds both the icon and accessible disclosure automatically. Consumers do not need to implement this separately. [id: ctalink.new-window-disclosure-auto; testability: auto]
- **CTALink** WILL append "noopener" to rel automatically for external links. Rationale: CTALink detects external links and appends "noopener" to rel automatically. Manually adding it is harmless but redundant. [id: ctalink.noopener-automatic; testability: auto]
- If opensInNewWindow is true: **CTALink** MUST inform users before they activate a link that opens in a new window. Rationale: Opening a new tab without warning disorients users, especially those using screen readers or keyboard navigation. [id: ctalink.new-window-disclosure; testability: auto]
- **CTALink** SHOULD use the opensInNewWindow prop rather than setting target="_blank" directly. Rationale: The prop sets the new-window icon, adds visually hidden "(opens in new window)" text for screen readers, and sets target="_blank" as a single coordinated action. Setting target="_blank" manually omits the icon and the accessible label. [id: ctalink.use-opens-in-new-window-prop; testability: auto]
## Accessibility
### In brief
CTA Link is a native `a` styled like a button. A screen reader announces its label and the role, link.
### What Nebula provides
- The browser's own link: its role, `Enter` activation and focus.
- A two-colour focus ring when the link has keyboard focus.
- The chevron and new window icons are hidden from screen readers.
- With `opensInNewWindow`, the hidden text "(opens in new window)" is part of the accessible name.
### Keyboard behaviour
The link is reached with `Tab` and followed with `Enter`. `Space` scrolls the page, as it does on every link.
#### Interactions
- **Tab** - Moves focus to the link
- **Enter** - Follows the link
### Accessible design
#### Write link text that says where it leads
Screen reader users often list every link on a page out of context, and several "View details" links tell them nothing. Where the visible text has to repeat, as in a table row, add the context after it as hidden text: "View details" plus "invoice 2024-03", for example with [Visually Hidden](/components/visually-hidden/).
WCAG: [2.4.4 Link Purpose (In Context)](https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html) [2.5.3 Label in Name](https://www.w3.org/WAI/WCAG22/Understanding/label-in-name.html)
#### Choose CTA Link to go somewhere and CTA Button to act here
Both look the same, but a screen reader announces one as a link and one as a button, and users expect a page change from the first and an action from the second. A keyboard user who presses `Space` on a link scrolls the page instead of acting.
WCAG: [4.1.2 Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html)
#### Name the action in a destructive secondary link
Red is the only other signal, and not everyone sees it: "Cancel my green plan", not "Cancel".
WCAG: [1.4.1 Use of Color](https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html)
### Accessible development
#### Always pass an `href`
An `a` without `href` has no link role and a keyboard user cannot reach it. With React Router, pass `to` together with `forwardedAs={Link}`, and see [Using React Router](#using-react-router).
WCAG: [2.1.1 Keyboard](https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html) [4.1.2 Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html)
#### Don't add `aria-label` to a CTA Link
It replaces the visible text as the name, which breaks speech input, and it drops the "(opens in new window)" text.
WCAG: [2.5.3 Label in Name](https://www.w3.org/WAI/WCAG22/Understanding/label-in-name.html)
#### Use `opensInNewWindow` instead of `target="_blank"`
All anchor props pass through, so `target="_blank"` works, but then the link opens a new window without the icon or the hidden text.
### Accessibility testing status
Reviewed on 9 October 2026 against v17.0.1.
| Test | Status | Tested with |
| --- | --- | --- |
| Default state | Tested | axe-core via the Storybook test runner (CI) |
| Advanced states | Partially tested | axe-core via the Storybook test runner (CI) |
| Keyboard navigation | Manually tested | keyboard (reported by the owner) |
| Screen reader | Partially tested | VoiceOver with Chrome (reported by the owner) |
What each test covers:
- Default state: The component as it first appears, before anything is clicked or changed.
- Advanced states: Every other state and variant: error, open, selected, each size and style, and hovering.
- Keyboard navigation: Whether every function can be reached and used with the keyboard alone, with a visible focus ring.
- Screen reader: Whether a screen reader announces what is on screen correctly: the name, the role and any state.
What these statuses mean:
- Tested: Every check in this row passed in an automated run that repeats on every pull request.
- Manually tested: Every check in this row was run once, by a person or with a tool they started, and all of them passed. It is not re-run on every change.
- Partially tested: Some checks in this row were run once and passed; the rest are still to do.
- Issue found: A check in this row failed. The ticket is shown beside the status.
- Not tested: No check in this row has been run yet.
- Not applicable: This row does not apply to the component, for the reason given beside it.
The table records which checks were run on this component on its own. It is not a statement of WCAG conformance, and it does not cover how the component is used.
## Properties
### CTALink
Also accepts all props from `AnchorHTMLAttributes`, `HTMLAttributes`.
- `iconLeft` (type `IconName | 'off' | (string & {})`, optional, default `'off'`). Name of an icon to show before the label. Use `'off'` for no icon.
- `iconRight` (type `IconName | 'off' | (string & {})`, optional, default `'chevron-right' ('new-window' when `opensInNewWindow` is set)`). Name of an icon to show after the label. Use `'off'` for no icon.
- `fullWidth` (type `'always' | 'never' | 'small'`, optional, default `'small'`). When the link fills the width of its container. `'small'` means full width on small screens only.
- `variant` (type `'primary' | 'secondary' | 'tertiary' | 'destructive' | 'destructive-secondary' | 'destructive-tertiary'`, optional, default `'primary'`). The visual style of the link.
- `opensInNewWindow` (type `boolean`, optional). Opens the link in a new tab (sets `target="_blank"`), adds a visually hidden "(opens in new window)" message for screen readers, and swaps the default right icon for a new-window icon.
- `size` (type `'default' | 'small'`, optional, default `'default'`). The size of the link.
- `as` (type `string | ComponentType`, optional, default `'a'`).
- `forwardedAs` (type `ElementType`, optional). Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it.
### PrimaryCTALink
Also accepts all props from `AnchorHTMLAttributes`, `HTMLAttributes`.
Always sets `variant` to `'primary'` (passing one has no effect).
- `iconLeft` (type `IconName | 'off' | (string & {})`, optional, default `'off'`). Name of an icon to show before the label. Use `'off'` for no icon.
- `iconRight` (type `IconName | 'off' | (string & {})`, optional, default `'chevron-right' ('new-window' when `opensInNewWindow` is set)`). Name of an icon to show after the label. Use `'off'` for no icon.
- `fullWidth` (type `'always' | 'never' | 'small'`, optional, default `'small'`). When the link fills the width of its container. `'small'` means full width on small screens only.
- `opensInNewWindow` (type `boolean`, optional). Opens the link in a new tab (sets `target="_blank"`), adds a visually hidden "(opens in new window)" message for screen readers, and swaps the default right icon for a new-window icon.
- `size` (type `'default' | 'small'`, optional, default `'default'`). The size of the link.
- `as` (type `string | ComponentType`, optional, default `'a'`).
- `forwardedAs` (type `ElementType`, optional). Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it.
### SecondaryCTALink
Also accepts all props from `AnchorHTMLAttributes`, `HTMLAttributes`.
Always sets `variant` to `'secondary'` (passing one has no effect).
- `iconLeft` (type `IconName | 'off' | (string & {})`, optional, default `'off'`). Name of an icon to show before the label. Use `'off'` for no icon.
- `iconRight` (type `IconName | 'off' | (string & {})`, optional, default `'chevron-right' ('new-window' when `opensInNewWindow` is set)`). Name of an icon to show after the label. Use `'off'` for no icon.
- `fullWidth` (type `'always' | 'never' | 'small'`, optional, default `'small'`). When the link fills the width of its container. `'small'` means full width on small screens only.
- `opensInNewWindow` (type `boolean`, optional). Opens the link in a new tab (sets `target="_blank"`), adds a visually hidden "(opens in new window)" message for screen readers, and swaps the default right icon for a new-window icon.
- `size` (type `'default' | 'small'`, optional, default `'default'`). The size of the link.
- `as` (type `string | ComponentType`, optional, default `'a'`).
- `forwardedAs` (type `ElementType`, optional). Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it.
### TertiaryCTALink
Also accepts all props from `AnchorHTMLAttributes`, `HTMLAttributes`.
Always sets `variant` to `'tertiary'` (passing one has no effect).
- `iconLeft` (type `IconName | 'off' | (string & {})`, optional, default `'off'`). Name of an icon to show before the label. Use `'off'` for no icon.
- `iconRight` (type `IconName | 'off' | (string & {})`, optional, default `'chevron-right' ('new-window' when `opensInNewWindow` is set)`). Name of an icon to show after the label. Use `'off'` for no icon.
- `fullWidth` (type `'always' | 'never' | 'small'`, optional, default `'small'`). When the link fills the width of its container. `'small'` means full width on small screens only.
- `opensInNewWindow` (type `boolean`, optional). Opens the link in a new tab (sets `target="_blank"`), adds a visually hidden "(opens in new window)" message for screen readers, and swaps the default right icon for a new-window icon.
- `size` (type `'default' | 'small'`, optional, default `'default'`). The size of the link.
- `as` (type `string | ComponentType`, optional, default `'a'`).
- `forwardedAs` (type `ElementType`, optional). Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it.
### DestructiveSecondaryCTALink
Also accepts all props from `AnchorHTMLAttributes`, `HTMLAttributes`.
Always sets `variant` to `'destructive-secondary'` (passing one has no effect).
- `iconLeft` (type `IconName | 'off' | (string & {})`, optional, default `'off'`). Name of an icon to show before the label. Use `'off'` for no icon.
- `iconRight` (type `IconName | 'off' | (string & {})`, optional, default `'chevron-right' ('new-window' when `opensInNewWindow` is set)`). Name of an icon to show after the label. Use `'off'` for no icon.
- `fullWidth` (type `'always' | 'never' | 'small'`, optional, default `'small'`). When the link fills the width of its container. `'small'` means full width on small screens only.
- `opensInNewWindow` (type `boolean`, optional). Opens the link in a new tab (sets `target="_blank"`), adds a visually hidden "(opens in new window)" message for screen readers, and swaps the default right icon for a new-window icon.
- `size` (type `'default' | 'small'`, optional, default `'default'`). The size of the link.
- `as` (type `string | ComponentType`, optional, default `'a'`).
- `forwardedAs` (type `ElementType`, optional). Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it.
### DestructiveTertiaryCTALink
Also accepts all props from `AnchorHTMLAttributes`, `HTMLAttributes`.
Always sets `variant` to `'destructive-tertiary'` (passing one has no effect).
- `iconLeft` (type `IconName | 'off' | (string & {})`, optional, default `'off'`). Name of an icon to show before the label. Use `'off'` for no icon.
- `iconRight` (type `IconName | 'off' | (string & {})`, optional, default `'chevron-right' ('new-window' when `opensInNewWindow` is set)`). Name of an icon to show after the label. Use `'off'` for no icon.
- `fullWidth` (type `'always' | 'never' | 'small'`, optional, default `'small'`). When the link fills the width of its container. `'small'` means full width on small screens only.
- `opensInNewWindow` (type `boolean`, optional). Opens the link in a new tab (sets `target="_blank"`), adds a visually hidden "(opens in new window)" message for screen readers, and swaps the default right icon for a new-window icon.
- `size` (type `'default' | 'small'`, optional, default `'default'`). The size of the link.
- `as` (type `string | ComponentType`, optional, default `'a'`).
- `forwardedAs` (type `ElementType`, optional). Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it.