On this page
CTA Link
Navigate users to another page or URL with a prominent, action-oriented link.
- Overview
- Rules and guidance
- Properties
- Styles
- Accessibility
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 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 instead. CTA Link is a standalone control and disrupts reading flow when placed inside body text.
Anatomy

- Label
- Right icon (default: chevron-right; can be suppressed or replaced)
- Left icon (optional)
Basic usage
Edit live code
<CTALink href="" variant="primary">Plant a tree</CTALink>
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.
Edit live code
<PrimaryCTALink href="">Plant a tree</PrimaryCTALink>
Secondary
Use for supporting navigational actions that sit alongside a primary button or link, such as "View details" next to "Get started".
Edit live code
<SecondaryCTALink href="">Find out more about trees</SecondaryCTALink>
Tertiary
Use for low-emphasis navigational actions that are available but not the focus of the page.
Edit live code
<TertiaryCTALink href="">See all options</TertiaryCTALink>
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.
Edit live code
<DestructiveSecondaryCTALink href="">Cancel my green plan</DestructiveSecondaryCTALink>
Destructive tertiary
Use for low-emphasis destructive navigation, when the risk needs to be communicated but the action should not be visually dominant.
Edit live code
<DestructiveTertiaryCTALink href="">Remove saved address</DestructiveTertiaryCTALink>
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.
Edit live code
<PrimaryCTALink href="" iconRight="off">View account details</PrimaryCTALink>
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.
Edit live code
<PrimaryCTALink href="" opensInNewWindow>More facts about trees</PrimaryCTALink>
Using React Router
By default, CTA Link renders as an <a> 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.
import { Link } from 'react-router-dom';
<PrimaryCTALink forwardedAs={Link} to="/account">
View account
</PrimaryCTALink>
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".
Edit live code
<PrimaryCTALink href="" iconLeft="download">Download statement</PrimaryCTALink>
The right icon can be replaced with any icon name. Pass the icon name to iconRight to override the default chevron.
Edit live code
<PrimaryCTALink href="" iconRight="download">Download statement</PrimaryCTALink>
Small size
Use size="small" for compact contexts such as table rows or dense UI where the default link height would be too large.
Edit live code
<PrimaryCTALink href="" size="small">View account details</PrimaryCTALink>
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
Edit live code
<PrimaryCTALink href="" fullWidth="always">Plant a tree</PrimaryCTALink>
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" |
usage
- CTALink must be used only for navigation to a URL, not to trigger same-page actionsCTALink renders as an <a> 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).Copy rule ID for ctalink.navigation-only
- CTALink must receive a valid href (or a to prop when used with a router component)An anchor without a href is not a link. It is not keyboard-focusable and provides no navigation target.Copy rule ID for ctalink.has-href
- CTALink must not use the 'destructive' variant (filled red background)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.Copy rule ID for ctalink.destructive-secondary-only
- CTALink must not use the loading propCTALink 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.Copy rule ID for ctalink.no-loading-prop
- CTALink should use the as prop with a router Link component for client-side navigation in single-page applicationsUsing 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.Copy rule ID for ctalink.router-link
- CTALink should not suppress the default chevron-right icon without a clear contextual reasonThe 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.Copy rule ID for ctalink.default-chevron
placement
- PrimaryCTALink should not appear more than once in a single section or cardMultiple primary actions compete for attention and dilute the hierarchy.Copy rule ID for ctalink.one-primary-per-section
content
- CTALink must have a label that is unique on the page, or include visually hidden text to distinguish it from other linksScreen 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.Copy rule ID for ctalink.unique-label
- CTALink must not use labels such as "Click here", "Here", "Read more", or "Learn more" without additional contextThese labels are meaningless when heard out of context by a screen reader user.Copy rule ID for ctalink.no-click-here
- CTALink should use sentence case for the labelCopy rule ID for ctalink.sentence-case
- CTALink should start the label with a verb or a noun that describes the destinationLabels 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.Copy rule ID for ctalink.active-language
- CTALink should use iconLeft only when the icon reinforces what the link leads toDecorative icons add visual noise without aiding comprehension.Copy rule ID for ctalink.icon-meaning
interaction
- CTALink will default iconRight to "chevron-right", or "new-window" when opensInNewWindow is true, unless iconRight is explicitly setCopy rule ID for ctalink.will-default-icon-right
- If opensInNewWindow is trueCTALink will add a visually hidden "(opens in new window)" disclosure and a new-window icon when opensInNewWindow is trueThe opensInNewWindow prop adds both the icon and accessible disclosure automatically. Consumers do not need to implement this separately.Copy rule ID for ctalink.new-window-disclosure-auto
- CTALink will append "noopener" to rel automatically for external linksCTALink detects external links and appends "noopener" to rel automatically. Manually adding it is harmless but redundant.Copy rule ID for ctalink.noopener-automatic
- If opensInNewWindow is trueCTALink must inform users before they activate a link that opens in a new windowOpening a new tab without warning disorients users, especially those using screen readers or keyboard navigation.Copy rule ID for ctalink.new-window-disclosure
- CTALink should use the opensInNewWindow prop rather than setting target="_blank" directlyThe 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.Copy rule ID for ctalink.use-opens-in-new-window-prop
CTALink
Also accepts all props from AnchorHTMLAttributes, HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
iconLeft | IconName | 'off' | (string & {}) | 'off' | No | Name of an icon to show before the label. Use `'off'` for no icon. |
iconRight | IconName | 'off' | (string & {}) | 'chevron-right' ('new-window' when `opensInNewWindow` is set) | No | Name of an icon to show after the label. Use `'off'` for no icon. |
fullWidth | always never small | 'small' | No | When the link fills the width of its container. `'small'` means full width on small screens only. |
variant | primary secondary tertiary destructive destructive-secondary destructive-tertiary | 'primary' | No | The visual style of the link. |
opensInNewWindow | boolean | — | No | 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 | default small | 'default' | No | The size of the link. |
as | string | ComponentType<any> | 'a' | No | — |
forwardedAs | ElementType | — | No | 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', so passing it has no effect.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
iconLeft | IconName | 'off' | (string & {}) | 'off' | No | Name of an icon to show before the label. Use `'off'` for no icon. |
iconRight | IconName | 'off' | (string & {}) | 'chevron-right' ('new-window' when `opensInNewWindow` is set) | No | Name of an icon to show after the label. Use `'off'` for no icon. |
fullWidth | always never small | 'small' | No | When the link fills the width of its container. `'small'` means full width on small screens only. |
opensInNewWindow | boolean | — | No | 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 | default small | 'default' | No | The size of the link. |
as | string | ComponentType<any> | 'a' | No | — |
forwardedAs | ElementType | — | No | 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', so passing it has no effect.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
iconLeft | IconName | 'off' | (string & {}) | 'off' | No | Name of an icon to show before the label. Use `'off'` for no icon. |
iconRight | IconName | 'off' | (string & {}) | 'chevron-right' ('new-window' when `opensInNewWindow` is set) | No | Name of an icon to show after the label. Use `'off'` for no icon. |
fullWidth | always never small | 'small' | No | When the link fills the width of its container. `'small'` means full width on small screens only. |
opensInNewWindow | boolean | — | No | 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 | default small | 'default' | No | The size of the link. |
as | string | ComponentType<any> | 'a' | No | — |
forwardedAs | ElementType | — | No | 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', so passing it has no effect.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
iconLeft | IconName | 'off' | (string & {}) | 'off' | No | Name of an icon to show before the label. Use `'off'` for no icon. |
iconRight | IconName | 'off' | (string & {}) | 'chevron-right' ('new-window' when `opensInNewWindow` is set) | No | Name of an icon to show after the label. Use `'off'` for no icon. |
fullWidth | always never small | 'small' | No | When the link fills the width of its container. `'small'` means full width on small screens only. |
opensInNewWindow | boolean | — | No | 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 | default small | 'default' | No | The size of the link. |
as | string | ComponentType<any> | 'a' | No | — |
forwardedAs | ElementType | — | No | 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', so passing it has no effect.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
iconLeft | IconName | 'off' | (string & {}) | 'off' | No | Name of an icon to show before the label. Use `'off'` for no icon. |
iconRight | IconName | 'off' | (string & {}) | 'chevron-right' ('new-window' when `opensInNewWindow` is set) | No | Name of an icon to show after the label. Use `'off'` for no icon. |
fullWidth | always never small | 'small' | No | When the link fills the width of its container. `'small'` means full width on small screens only. |
opensInNewWindow | boolean | — | No | 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 | default small | 'default' | No | The size of the link. |
as | string | ComponentType<any> | 'a' | No | — |
forwardedAs | ElementType | — | No | 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', so passing it has no effect.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
iconLeft | IconName | 'off' | (string & {}) | 'off' | No | Name of an icon to show before the label. Use `'off'` for no icon. |
iconRight | IconName | 'off' | (string & {}) | 'chevron-right' ('new-window' when `opensInNewWindow` is set) | No | Name of an icon to show after the label. Use `'off'` for no icon. |
fullWidth | always never small | 'small' | No | When the link fills the width of its container. `'small'` means full width on small screens only. |
opensInNewWindow | boolean | — | No | 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 | default small | 'default' | No | The size of the link. |
as | string | ComponentType<any> | 'a' | No | — |
forwardedAs | ElementType | — | No | 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. |
This component has no styles of its own to document.
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.
WCAG: 2.4.4 Link Purpose (In Context) 2.5.3 Label in Name
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
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
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.
WCAG: 2.1.1 Keyboard 4.1.2 Name, Role, Value
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
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 these tests and statuses mean
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.
The tests
- 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.
The statuses
- 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.