Set up in a few minutes, and yours to take with you.
import type { ReactNode } from "react";
const action = "inline-flex h-12 items-center justify-center gap-2 rounded-[calc(var(--tui-cta-radius)_*_0.4)] px-6 text-[15px] font-semibold no-underline transition-[filter,background-color,scale] duration-(--tui-duration) active:scale-[0.98]";
// The first button is cut from the band's text colour and lifted off it; the second is a pane of glass on it.
const lifted =
"bg-(--tui-cta-band-fg) text-(--tui-cta-band-bg) [background-image:linear-gradient(to_bottom,rgb(255_255_255_/_0.5),rgb(255_255_255_/_0)_50%,rgb(0_0_0_/_0.08))] [box-shadow:inset_0_1px_0_rgb(255_255_255_/_0.6),inset_0_-1px_0_rgb(0_0_0_/_0.14),0_1px_2px_rgb(0_0_0_/_0.25),0_14px_28px_-12px_rgb(0_0_0_/_0.55)] hover:brightness-95";
const glass =
"bg-[color-mix(in_srgb,var(--tui-cta-band-fg)_10%,transparent)] text-(--tui-cta-band-fg) backdrop-blur-sm [box-shadow:inset_0_0_0_1px_color-mix(in_srgb,var(--tui-cta-band-fg)_22%,transparent),inset_0_1px_0_color-mix(in_srgb,var(--tui-cta-band-fg)_18%,transparent)]";
export interface CtaProps {
/** A short line in a capsule over the headline, and a word to mark it with. */
eyebrow?: ReactNode;
tag?: string;
title: ReactNode;
description?: ReactNode;
primary?: { label: string; href: string };
secondary?: { label: string; href: string };
/** A line of proof under the buttons: initials of a few people, and what to say beside them. */
proof?: { people: string[]; text: ReactNode };
className?: string;
}
/**
* The ask, at the end of a page. A filled band set in a shell, with rings
* rising from its lower edge and light pooling in two corners, so a flat
* colour reads as a lit surface. The first button is lifted off it and the
* second is glass on it.
*/
export function Cta({ eyebrow, tag, title, description, primary, secondary, proof, className }: CtaProps) {
return (
<section data-slot="cta" className={["flex w-full justify-center bg-(--tui-cta-bg) px-6 py-(--tui-cta-padding-y) font-(family-name:--tui-font)", className].filter(Boolean).join(" ")}>
<div data-slot="cta-shell" className="w-full max-w-(--tui-cta-width) rounded-(--tui-cta-radius) bg-(--tui-cta-shell-bg) p-1.5 [box-shadow:inset_0_0_0_1px_var(--tui-cta-border)]">
<div
data-slot="cta-band"
className="relative isolate flex flex-col items-center gap-6 overflow-hidden rounded-[calc(var(--tui-cta-radius)_-_6px)] bg-(--tui-cta-band-bg) px-8 py-(--tui-cta-band-padding) text-center text-(--tui-cta-band-fg) [background-image:linear-gradient(to_bottom,rgb(255_255_255_/_0.1),rgb(255_255_255_/_0)_40%,rgb(0_0_0_/_0.22))] [box-shadow:inset_0_1px_0_rgb(255_255_255_/_0.22),inset_0_0_0_1px_rgb(255_255_255_/_0.06),0_1px_2px_rgb(0_0_0_/_0.2),0_30px_60px_-30px_var(--tui-cta-band-bg)]"
>
<span
aria-hidden
data-slot="cta-rings"
className="pointer-events-none absolute bottom-0 left-1/2 -z-10 size-[72rem] -translate-x-1/2 translate-y-[58%] rounded-full opacity-(--tui-cta-pattern-strength) [background-image:repeating-radial-gradient(circle_at_center,transparent_0_63px,currentColor_63px_64px)] [mask-image:linear-gradient(to_bottom,black,transparent_46%)]"
/>
<span aria-hidden className="pointer-events-none absolute -top-40 -left-24 -z-10 size-[26rem] rounded-full bg-(--tui-cta-light) blur-3xl" />
<span aria-hidden className="pointer-events-none absolute -right-28 -bottom-44 -z-10 size-[30rem] rounded-full bg-(--tui-cta-light) blur-3xl" />
{eyebrow && (
<span data-slot="cta-eyebrow" className={`inline-flex h-8 items-center gap-2.5 rounded-full pr-3.5 pl-1 text-[13px] font-medium ${glass}`}>
{tag && <span className="inline-flex h-6 items-center rounded-full bg-(--tui-cta-band-fg) px-2.5 text-[11px] font-semibold tracking-[0.04em] text-(--tui-cta-band-bg) uppercase [box-shadow:0_1px_2px_rgb(0_0_0_/_0.25)]">{tag}</span>}
{eyebrow}
</span>
)}
<h2 className="m-0 max-w-[42rem] text-(length:--tui-cta-title-size) leading-[1.04] font-semibold tracking-[-0.035em] text-balance">{title}</h2>
{description && <p className="m-0 max-w-[32rem] text-[18px] leading-relaxed text-pretty opacity-75">{description}</p>}
{(primary || secondary) && (
<div className="mt-2 flex flex-wrap items-center justify-center gap-3">
{primary && (
<a href={primary.href} data-slot="cta-primary" className={`${action} ${lifted}`}>
{primary.label}
<svg aria-hidden viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" className="size-4">
<path d="M3.500 8h9M8.500 4l4 4-4 4" />
</svg>
</a>
)}
{secondary && (
<a href={secondary.href} data-slot="cta-secondary" className={`${action} ${glass} hover:bg-[color-mix(in_srgb,var(--tui-cta-band-fg)_16%,transparent)]`}>
{secondary.label}
</a>
)}
</div>
)}
{proof && (
<div data-slot="cta-proof" className="mt-2 flex items-center gap-3 text-[14px]">
<span aria-hidden className="flex">
{proof.people.slice(0, 5).map((initials, i) => (
<span key={i} className="-ml-2 flex size-8 items-center justify-center rounded-full bg-[color-mix(in_srgb,var(--tui-cta-band-fg)_20%,var(--tui-cta-band-bg))] text-[11px] font-semibold first:ml-0 [box-shadow:0_0_0_2px_var(--tui-cta-band-bg)]">
{initials}
</span>
))}
</span>
<span className="opacity-75">{proof.text}</span>
</div>
)}
</div>
</div>
</section>
);
}:root {
color-scheme: light;
--tui-bg: #ffffff;
--tui-fg: #0a0a0a;
--tui-muted: #f5f5f5;
--tui-muted-fg: #737373;
--tui-border: #e5e5e5;
--tui-primary: #171717;
--tui-primary-fg: #fafafa;
--tui-ring: #a3a3a3;
--tui-success: #16a34a;
--tui-warning: #d97706;
--tui-danger: #dc2626;
--tui-radius: 8px;
--tui-border-width: 1px;
--tui-space: 4px;
--tui-shadow: none;
--tui-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--tui-text: 14px;
--tui-duration: 150ms;
/* Call to action */
--tui-cta-padding-y: calc(var(--tui-space) * 12);
--tui-cta-band-padding: calc(var(--tui-space) * 24);
--tui-cta-width: 1040px;
--tui-cta-radius: calc(var(--tui-radius) * 4);
--tui-cta-title-size: 52px;
--tui-cta-pattern-strength: 0.16;
--tui-cta-bg: var(--tui-bg);
--tui-cta-shell-bg: var(--tui-muted);
--tui-cta-border: var(--tui-border);
--tui-cta-band-bg: var(--tui-primary);
--tui-cta-band-fg: var(--tui-primary-fg);
--tui-cta-light: color-mix(in srgb, var(--tui-primary-fg) 22%, transparent);
}
.dark,
[data-theme="dark"] {
color-scheme: dark;
--tui-bg: #0a0a0a;
--tui-fg: #fafafa;
--tui-muted: #1c1c1c;
--tui-muted-fg: #a3a3a3;
--tui-border: #2a2a2a;
--tui-primary: #fafafa;
--tui-primary-fg: #171717;
--tui-ring: #737373;
--tui-success: #22c55e;
--tui-warning: #f59e0b;
--tui-danger: #ef4444;
}Needs Tailwind CSS v4. Import the stylesheet once, after Tailwind.