CTA Button

( Cta / Link / Action )

A capsule call-to-action link: a blurred translucent ring around a gradient pill, with a white arrow disc pinned to its right edge.

Live preview and source code for CTA Button

A call-to-action link built as a capsule: a gradient pill lit by a pair of inset highlights, with a white arrow disc pinned to its right edge and a blurred translucent ring around the whole thing. Hovering swaps the pill end for end — the label slides right, the disc slides out past the edge, and a second one slides in from the left. One file of inline styles, no dependencies beyond React.

Installation

Copy the source from the Code tab above into your project as CtaButton.tsx. There is nothing to install: the arrow is an inline SVG, every style is inline, and the file imports nothing but React.

The label is set in Inter. If your app does not load it, the button falls back to your system sans and the pill resizes to fit — nothing breaks, but the proportions shift a little.

Usage

Every prop is optional, but href is the one you actually want:

Example.tsx
import CtaButton from "./components/CtaButton";

export default function Example() {
    return <CtaButton label="View all features" href="/features" size="lg" />;
}

When to reach for it

The single primary action in a dark hero, where the page has one job and the button has to carry it. lg with tone="blue" is built for that position.

The link that closes a section: a feature grid ending in "View all features", a pricing table ending in "Talk to our team". md sits under a grid without competing with the heading above it.

A navbar action on a dark bar. sm drops the outer ring entirely and shrinks to a bare pill, which keeps it from crowding the links beside it.

When not to

Not over a photograph or a busy pattern. The ring is a white hairline floating on the shadow the pill casts, and the label is white on a dark pill. Both need a surface that holds still underneath them. A plain page works in either direction — on a light one the shadow is exactly what makes the ring read — but over an image the hairline lands on whatever happens to be behind it and the frame breaks up.

Not for anything that is not navigation. This renders an <a> and no prop changes the element. Submitting a form or firing a handler wants a real <button>, and Button is the better call.

Not when the label is long. The pill is min-content wide with the label set to pre, so it grows rather than wraps. Past four or five words it runs off a narrow screen.

Not in a dense row of links. The hover runs for 400ms and moves three elements. One of these ending a section earns that; six in a footer is a lot of motion for links nobody is reading yet.

Accessibility

The arrows and the hairlines are aria-hidden, so the label is the entire accessible name. It has to say where the link goes on its own, which is why the default is "View all features" and not "Learn more".

An <a> with no href is not focusable and is not announced as a link, so passing href is what makes this keyboard-reachable at all. The focus outline is left alone, and the arrow swap runs on :focus-visible, so tabbing to the button shows the same state the mouse does while clicking it does not.

The animation honours prefers-reduced-motion: with it set the arrows still swap, instantly rather than over 400ms.

tone="blue" is worth checking before you ship it. White on the light end of that gradient measures about 3.7:1, and the label sets at 600 weight, which WCAG does not count as bold — so it does not reach AA at any size. Treat it as a large display control rather than something you would set a sentence in. tone="dark" clears 12:1.

Props

Every prop the CTA Button component accepts, with its type and default value.

Props accepted by the CTA Button component
PropTypeDefaultDescription
labelstringView all featuresText rendered inside the pill. A single line: it never wraps.
hrefstringDestination for the link. Without it the anchor is not focusable and not announced as a link.
size"sm" | "md" | "lg"mdVisual size. sm drops the translucent ring and sets the label at 14px; md and lg keep the ring at 18px.
tone"dark" | "blue"darkColour of the inner pill. dark is a near-black gradient, blue a saturated one.
classNamestringExtra classes on the anchor, for consumers using a utility CSS framework.

Notes

style is spread after the component's own inline styles, so anything you pass wins. Every other anchor prop lands on the <a>, so target, rel, data-* and event handlers pass straight through.

Both discs are always in the DOM; only their transform changes. The pill clips its own overflow, which is what hides the one waiting outside — override overflow on it and you get a stray arrow parked beside the button.

Each hairline is the last child of the box it outlines, and has to stay there. Moved above the pill in the markup, the ring's line is painted over by the pill's shadow and the ring vanishes on any surface pale enough to show that shadow.

The label pins its own letter-spacing to 0em rather than inheriting. On a page with tight tracking that is the difference between the proportions the button was drawn with and a noticeably narrower pill.