CTA Button
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:
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.
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | View all features | Text rendered inside the pill. A single line: it never wraps. |
href | string | Destination for the link. Without it the anchor is not focusable and not announced as a link. | |
size | "sm" | "md" | "lg" | md | Visual size. sm drops the translucent ring and sets the label at 14px; md and lg keep the ring at 18px. |
tone | "dark" | "blue" | dark | Colour of the inner pill. dark is a near-black gradient, blue a saturated one. |
className | string | Extra 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.