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-plus

layer

core / patterns

intensity

structures · organizes a region, stays out of the content

composition

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 panel

a11y

the title is a real heading at headingLevel · the footer rule is aria-hiddenjest-axe
live 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
usagecopy
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

proptypedefaultnotes
titleReactNodethe panel headline.
bodyReactNodesupporting copy.
headingLevel"h2" | "h3" | "h4""h2"heading element for the title. match the page outline.
actionReactNodethe call to action. pass a Button.
footerReactNodetrailing proof row. draws a rule above itself.
footerLabelReactNodemono kicker beside the footer content.

get it

npx shadcn add https://usva.build/r/cta-banner.jsoncopy
source · components/ui/cta-banner.tsxexactly what this command copiescopy
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";