docs / patterns / cta-banner
CtaBanner
the panel that closes a marketing page: a title, a line of copy, and the one action you want next. everything below the title is optional, so it degrades to just a headline and a button.
intensity · structuresrsc · server
provenance
shaped from sisu-pluslayer
core / patternsintensity
structures · organizes a region, stays out of the contentcomposition
the last section of a landing page, before the footera proof row of Chips under the action when you have receipts to shownot mid-page. one per page, at the closenever nest one inside a Card. it is already a panela11y
the title is a real heading atheadingLevel · the footer rule is aria-hiddenjest-axelive demo · try it out
Have something in mind?
Design engineering for teams that sweat the details. A short call is the fastest way to start.
Recent work
FintechHealthDeveloper tools
customize
headingLevelmatch the page outline
footertrailing proof row, drawn under a rule
usage
import { CtaBanner } from "usva/patterns/cta-banner";
import { Button } from "usva/primitives/button";
import { Chip } from "usva/primitives/chip";
<CtaBanner
title="Have something in mind?"
body="Design engineering for teams that sweat the details. A short call is the fastest way to start."
action={<Button>Start a project</Button>}
footerLabel="Recent work"
footer={<><Chip>Fintech</Chip><Chip>Health</Chip></>}
/>props
| prop | type | default | notes |
|---|---|---|---|
| title | ReactNode | — | the panel headline. |
| body | ReactNode | — | supporting copy. |
| headingLevel | "h2" | "h3" | "h4" | "h2" | heading element for the title. match the page outline. |
| action | ReactNode | — | the call to action. pass a Button. |
| footer | ReactNode | — | trailing proof row. draws a rule above itself. |
| footerLabel | ReactNode | — | mono kicker beside the footer content. |
get it
npx shadcn add https://usva.build/r/cta-banner.jsonsource · components/ui/cta-banner.tsxexactly what this command copies
import * as React from "react";
import { cn } from "@/lib/utils";
export type CtaBannerHeadingLevel = "h2" | "h3" | "h4";
export interface CtaBannerProps
extends Omit<React.HTMLAttributes<HTMLElement>, "title"> {
title: React.ReactNode;
body?: React.ReactNode;
headingLevel?: CtaBannerHeadingLevel;
/** The call to action itself. Pass a Button. */
action?: React.ReactNode;
/** Mono kicker beside the footer content. */
footerLabel?: React.ReactNode;
/** Trailing proof row, e.g. a set of Chips. Draws a rule above itself. */
footer?: React.ReactNode;
}
/**
* The closing panel of a marketing page. The accent wash is an inline gradient rather
* than an arbitrary Tailwind value so it survives the registry copy without depending
* on the consumer's Tailwind config seeing the class.
*/
export const CtaBanner = React.forwardRef<HTMLElement, CtaBannerProps>(
(
{
className,
title,
body,
headingLevel: Heading = "h2",
action,
footerLabel,
footer,
...props
},
ref,
) => (
<section
ref={ref}
style={{
backgroundImage:
"linear-gradient(135deg, color-mix(in oklab, var(--color-accent-alt) 7%, transparent), color-mix(in oklab, var(--color-accent-alt) 2%, transparent) 50%, transparent 100%)",
}}
className={cn(
"relative isolate overflow-hidden rounded-2xl border border-accent-alt/15 px-8 py-7 max-md:px-5",
className,
)}
{...props}
>
<div className="flex flex-wrap items-start justify-between gap-8">
<div className="min-w-0">
<Heading className="mb-2 text-[1.1rem] font-bold leading-tight text-ink">
{title}
</Heading>
{body != null && (
<p className="max-w-2xl text-sm leading-6 text-muted">{body}</p>
)}
</div>
{action}
</div>
{footer != null && (
<>
<div
data-cta-rule=""
aria-hidden="true"
className="my-5 h-px bg-border"
/>
<div className="flex flex-wrap items-center gap-3">
{footerLabel != null && (
<span className="whitespace-nowrap font-mono text-[0.68rem] font-bold uppercase tracking-widest text-muted">
{footerLabel}
</span>
)}
<div className="flex flex-wrap gap-1.5">{footer}</div>
</div>
</>
)}
</section>
),
);
CtaBanner.displayName = "CtaBanner";