Site
Call to action
Closing action sections for the home page and product pages.
$ npx context-ds add ctaUsage
Use it for
The last section before the footer. ClosingCta on the home page with a pill-outline button and a CtaTextLink; Cta on inner pages with a CtaButton light and a CtaButton glass, the water texture as background.
Avoid
- Two call to action bands on one page
- Bold headings
- More than two actions
- A black band without the water
Dependencies: section, @radix-ui/react-slot
Example
examples/cta.tsx
import { SiteSection } from "@/components/site/section";import { ArrowRight } from "lucide-react";import { Button } from "@/components/ui/button";import { ClosingCta, Cta, CtaButton, CtaTextLink } from "@/components/site/cta";export function Site() { return (<SiteSection> <ClosingCta title="Start building with live web context." description="Scrape, search, and research the web with one API. Bring the results straight into your product." figure={<span className="font-data text-h1 tabular-nums">1,000</span>} figureLabel="free credits" note="Every month. No credit card required." primaryAction={ <Button asChild variant="pill-outline"> <a href="https://context.dev/signup">Start for free</a> </Button> } secondaryAction={ <CtaTextLink href="https://docs.context.dev"> Read the docs <ArrowRight aria-hidden="true" /> </CtaTextLink> } /> </SiteSection>);}export function Band() { return ( <Cta title="Ship an agent that actually" accent="knows things." description="Free tier, 10-minute integration, and the same API powering agents at Mintlify, daily.dev, and Propane. No credit card to start." primaryAction={<CtaButton href="https://context.dev/signup">Get API Access</CtaButton>} secondaryAction={ <CtaButton tone="glass" href="https://cal.com/context-dev/30min"> Book a call </CtaButton> } /> );}Source
components/site/cta.tsx
import * as React from "react";import { Slot } from "@radix-ui/react-slot";import { SectionEdgeLines } from "@/components/site/section";import { cn } from "@/lib/utils";// The soft blue ring around a white card. It wraps the card rather than being a shadow on it, so its outer edge keeps// the card's radius; the negative margin keeps it from adding layout size.const GLASS_RING = "-m-2 rounded-card bg-ring-tint p-2";const CARD = "rounded-card bg-surface ring-1 ring-hairline";/** * The home page's closing call to action: a white card in the soft blue ring, the heading and a line of muted text on * the left, and a smaller ringed card on the right with the offer (a figure, its label, a note) and the actions. * `background` sits behind the card's content (the site draws its dot field there). */function ClosingCta({ title, description, figure, figureLabel, note, primaryAction, secondaryAction, background, className, ...props}: Omit<React.ComponentProps<"section">, "title"> & { title: React.ReactNode; description?: React.ReactNode; /** The offer's large figure (the site sets it in dots, as wide as the card up to 20rem). */ figure?: React.ReactNode; figureLabel?: React.ReactNode; note?: React.ReactNode; /** A full-width `pill-outline` button. */ primaryAction?: React.ReactNode; /** A quiet text link under it (a CtaTextLink). */ secondaryAction?: React.ReactNode; background?: React.ReactNode;}) { const titleId = React.useId(); const hasOffer = figure || figureLabel || note || primaryAction || secondaryAction; return ( <section id="cta" data-slot="closing-cta" aria-labelledby={titleId} className={cn("relative w-full bg-surface py-16 md:py-20", className)} {...props}> <SectionEdgeLines /> <div className="relative mx-auto w-full max-w-(--ds-frame-column) px-6 sm:px-8 md:px-10 lg:px-12"> <div className={GLASS_RING}> <div className={cn("relative isolate grid items-center gap-8 overflow-hidden p-6 sm:p-8 lg:grid-cols-[1fr_calc(var(--spacing)*84)] lg:gap-12 lg:p-12", CARD)}> {background && ( <div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10 text-fg-brand"> {background} </div> )} <div className="relative flex flex-col items-start gap-5"> <h2 id={titleId} className="max-w-136 text-h3 leading-tight font-normal tracking-tight text-balance text-fg sm:text-h2"> {title} </h2> {description && <p className="max-w-md text-body leading-relaxed text-fg-muted sm:text-body-lg">{description}</p>} </div> {hasOffer && ( <div className={GLASS_RING}> <div className={cn("relative flex flex-col p-6 sm:p-7", CARD)}> {(figure || figureLabel) && ( <p className="flex flex-col items-center text-center text-fg"> {figure && <span className="flex w-full max-w-80 justify-center">{figure}</span>} {figureLabel && <span className="mt-2 text-h5 leading-snug">{figureLabel}</span>} </p> )} {note && <p className="mt-3 text-center text-body-sm leading-relaxed text-fg-muted">{note}</p>} {primaryAction && <div className="mt-6 flex flex-col [&>*]:w-full">{primaryAction}</div>} {secondaryAction && <div className="mt-2 flex flex-col">{secondaryAction}</div>} </div> </div> )} </div> </div> </div> </section> );}/** The closing card's quiet link: muted, darkening and underlining on hover, with an arrow and a 44px target. */function CtaTextLink({ asChild = false, className, children, ...props }: React.ComponentProps<"a"> & { asChild?: boolean }) { const Comp = asChild ? Slot : "a"; return ( <Comp data-slot="cta-text-link" className={cn( "inline-flex min-h-11 items-center justify-center gap-2 rounded-card text-body-sm leading-5 font-medium text-fg/70 underline-offset-4 transition-colors duration-150 ease-out outline-none hover:text-fg hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring motion-reduce:transition-none [&_svg]:size-4", className, )} {...props} > {children} </Comp> );}// The band's frame: dashed half-pixel hairlines in translucent white, 4px dashes with 4px gaps.const DASH_X = "bg-[repeating-linear-gradient(to_right,var(--ds-color-ring-on-brand)_0_var(--ds-space-1),transparent_var(--ds-space-1)_var(--ds-space-2))]";const DASH_Y = "bg-[repeating-linear-gradient(to_bottom,var(--ds-color-ring-on-brand)_0_var(--ds-space-1),transparent_var(--ds-space-1)_var(--ds-space-2))]";const NODE = "absolute bottom-16 size-1.5 translate-y-1/2 rounded-pill bg-white shadow-[0_0_0_var(--ds-space-0-5)_color-mix(in_srgb,var(--ds-color-white)_20%,transparent)]";/** * The call to action band: the dimmed brand-blue water band (pass the water texture as `background`; without one it * is the plain dimmed blue), a dashed frame down the column's edges with a white node where a dashed rule crosses them * near the foot, a centered heading with its closing words in light blue, a line of text and two actions (a CtaButton * `light` and a CtaButton `glass`). */function Cta({ title, accent, description, primaryAction, secondaryAction, background, className, ...props}: Omit<React.ComponentProps<"section">, "title"> & { title: React.ReactNode; /** The heading's closing words, in light blue. */ accent?: React.ReactNode; description?: React.ReactNode; primaryAction?: React.ReactNode; secondaryAction?: React.ReactNode; /** The water texture; it is dimmed so white copy stays legible. */ background?: React.ReactNode;}) { return ( <section id="cta" data-slot="cta" className={cn("relative isolate flex w-full items-center justify-center overflow-hidden bg-dark text-white", className)} {...props}> <div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10 bg-blue-80 brightness-80"> {background} </div> <div aria-hidden="true" className="pointer-events-none absolute inset-0 hidden md:block"> <div className={cn("absolute inset-x-0 bottom-16 h-px scale-y-50", DASH_X)} /> </div> <div aria-hidden="true" className="pointer-events-none absolute inset-0 hidden justify-center md:flex"> <div className="relative h-full w-full max-w-(--ds-frame-column)"> <div className={cn("absolute inset-y-0 left-0 w-px scale-x-50", DASH_Y)} /> <div className={cn("absolute inset-y-0 right-0 w-px scale-x-50", DASH_Y)} /> <span className={cn(NODE, "left-0 -translate-x-1/2")} /> <span className={cn(NODE, "right-0 translate-x-1/2")} /> </div> </div> <div className="relative z-10 mx-auto w-full max-w-(--ds-frame-column) px-6 py-16 md:px-8 md:py-20"> <div className="mx-auto flex max-w-2xl flex-col items-center gap-6 text-center"> <div className="flex w-full flex-col items-center gap-4"> <h2 className="text-h3 leading-tight font-normal tracking-tight text-balance text-white md:text-h2"> {title} {accent && ( <> {" "} <span className="text-blue-40">{accent}</span> </> )} </h2> </div> {description && <p className="max-w-130 text-body-lg leading-relaxed text-white/90">{description}</p>} {(primaryAction || secondaryAction) && ( <div className="relative flex flex-col items-center justify-center gap-4 pt-4 sm:flex-row"> {primaryAction} {secondaryAction} </div> )} </div> </div> </section> );}/** * The band's two buttons, one shape (the brand's button radius, 48px tall): `light` is the white key with a soft white * glow (it dims a step on hover); `glass` is the frosted key beside it. */function CtaButton({ tone = "light", asChild = false, className, ...props }: React.ComponentProps<"a"> & { tone?: "light" | "glass"; asChild?: boolean }) { const Comp = asChild ? Slot : "a"; return ( <Comp data-slot="cta-button" className={cn( "relative isolate inline-flex cursor-pointer items-center justify-center gap-2 overflow-hidden whitespace-nowrap text-body font-medium outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring active:scale-[0.97] motion-reduce:transition-none motion-reduce:active:scale-100", tone === "light" && "h-12 rounded-control border border-black/10 bg-white px-6.5 leading-snug text-fg shadow-[0_0_0_1px_color-mix(in_srgb,var(--ds-color-white)_35%,transparent),0_var(--ds-space-3)_var(--ds-space-10)_color-mix(in_srgb,var(--ds-color-white)_12%,transparent)] transition-[filter,scale] duration-150 ease-out hover:brightness-97", tone === "glass" && "h-12 rounded-control bg-white/16 px-6 leading-snug text-white shadow-key outline-1 -outline-offset-1 outline-white/12 backdrop-blur-xs transition-[background-color,scale] duration-200 ease-out hover:bg-white/24", className, )} {...props} /> );}export { ClosingCta, Cta, CtaButton, CtaTextLink };Reference: src/components/sections/home/closing-cta.tsx; src/components/sections/cta.tsx; src/components/sections/shared/cta-background.tsx; src/components/sections/shared/dark-cta-link.tsx.