Skip to content
Facade UI
Colour mode
GitHub (opens in a new tab)
blocksection

CTA band

The closing call to action, in a muted, card, plain or inverted-primary surface.

Also ships a motion variant: CTA band (motion). The static one is the default.

Preview

Open full width (opens in a new tab)
Preview width

Installation

Package manager
pnpm dlx shadcn@latest add https://facadeui.dev/r/cta-band.json

Pulls in container, cta-group, eyebrow, heading, section, types, utils. The CLI installs them for you.

Usage

The exact source of the preview above.

demos/cta-band.tsx
import { CtaBand } from "@registry/sections/cta-band"

import { ACTIONS } from "./content"

export function Demo() {
  return (
    <div className="flex flex-col gap-10">
      <CtaBand
        title="Ready when you are"
        description="Install the first section and see how it sits in your own theme."
        actions={ACTIONS}
        note="No account required."
        spacing="none"
      />
      <CtaBand
        variant="primary"
        layout="split"
        title="Or start from a template"
        description="Whole pages, assembled from the same sections."
        actions={[{ label: "Browse templates", href: "#templates" }]}
        spacing="none"
      />
    </div>
  )
}

Source

What the CLI copies into your project, byte for byte.

components/sections/cta-band.tsx
/**
 * CtaBand — the closing call to action.
 *
 * a11y: `variant="primary"` inverts the band, so it pairs `--primary` with
 * `--primary-foreground` rather than reaching for an arbitrary tint. That is the
 * one colour pair the token layer guarantees at 4.5:1 in every preset and both
 * modes, and `scripts/check-contrast.ts` keeps it that way.
 *
 * Dependencies: react, @/lib/types, @/lib/utils,
 * @/components/ui/container, @/components/ui/cta-group, @/components/ui/eyebrow,
 * @/components/ui/heading, @/components/ui/section.
 */

import type { ElementType, ReactNode } from "react"

import type {
  CtaItem,
  HeadingLevel,
  LinkComponent,
  StackSlotProps,
} from "@/lib/types"
import { cn, slugId } from "@/lib/utils"
import { Container } from "@/components/ui/container"
import { CtaGroup } from "@/components/ui/cta-group"
import { Eyebrow } from "@/components/ui/eyebrow"
import { Heading } from "@/components/ui/heading"
import { Section } from "@/components/ui/section"

export interface CtaBandProps extends StackSlotProps {
  title: string
  description?: string
  eyebrow?: ReactNode
  actions?: CtaItem[]
  link?: LinkComponent
  note?: ReactNode
  /** `muted` and `card` sit inside the container; `primary` inverts the band. */
  variant?: "muted" | "card" | "primary" | "plain"
  /** `center` stacks everything; `split` puts the buttons beside the copy. */
  layout?: "center" | "split"
  headingLevel?: HeadingLevel
  as?: "section" | "div"
  spacing?: "sm" | "md" | "lg" | "none"
  className?: string
  id?: string
}

const surfaces = {
  muted: "bg-muted text-foreground",
  card: "bg-card text-card-foreground border",
  primary: "bg-primary text-primary-foreground",
  plain: "",
} as const

export function CtaBand({
  title,
  description,
  eyebrow,
  actions,
  link,
  note,
  variant = "muted",
  layout = "center",
  stackAs,
  blockAs,
  headingLevel = 2,
  as = "section",
  spacing = "md",
  className,
  id,
}: CtaBandProps) {
  const Stack = (stackAs ?? "div") as ElementType
  const Block = (blockAs ?? "div") as ElementType
  const titleId = id ? `${id}-title` : slugId(title)
  const inverted = variant === "primary"
  const centered = layout === "center"

  return (
    <Section
      as={as}
      spacing={spacing}
      labelledBy={as === "section" ? titleId : undefined}
      id={id}
      className={className}
    >
      <Container>
        <div
          className={cn(
            "rounded-2xl px-6 py-14 sm:px-12",
            surfaces[variant],
            variant === "plain" && "px-0 py-0",
          )}
        >
          <Stack
            className={cn(
              "flex flex-col gap-6",
              centered
                ? "items-center text-center"
                : "lg:flex-row lg:items-center lg:justify-between lg:gap-12 lg:text-left",
            )}
          >
            <Block
              className={cn(
                "flex flex-col gap-4",
                centered ? "items-center" : "max-w-2xl",
              )}
            >
              {eyebrow ? (
                <Eyebrow
                  tone={inverted ? "foreground" : "primary"}
                  className={inverted ? "text-primary-foreground/80" : undefined}
                >
                  {eyebrow}
                </Eyebrow>
              ) : null}

              <Heading
                level={headingLevel}
                id={titleId}
                className="text-display-sm text-balance font-semibold"
              >
                {title}
              </Heading>

              {description ? (
                <p
                  className={cn(
                    "text-pretty text-lg",
                    inverted ? "text-primary-foreground/85" : "text-muted-foreground",
                    centered && "max-w-2xl",
                  )}
                >
                  {description}
                </p>
              ) : null}
            </Block>

            {actions?.length ? (
              <Block
                className={cn(
                  "flex flex-col gap-3",
                  centered && "w-full items-center sm:w-auto",
                )}
              >
                <CtaGroup
                  items={
                    inverted
                      ? actions.map((action, index) => ({
                          ...action,
                          // On an inverted band the theme's primary button would
                          // disappear into the surface it sits on.
                          variant:
                            action.variant ?? (index === 0 ? "secondary" : "ghost"),
                        }))
                      : actions
                  }
                  link={link}
                  size="lg"
                  align={centered ? "center" : "start"}
                  stackOnMobile
                />
                {note ? (
                  <p
                    className={cn(
                      "text-pretty text-sm",
                      inverted ? "text-primary-foreground/75" : "text-muted-foreground",
                      centered && "text-center",
                    )}
                  >
                    {note}
                  </p>
                ) : null}
              </Block>
            ) : null}
          </Stack>
        </div>
      </Container>
    </Section>
  )
}

Props

CtaBandProps

Extends StackSlotProps.

Props for CtaBandProps
PropTypeDefault
title*Requiredstring
descriptionstring
eyebrowReactNode
actionsCtaItem[]
linkLinkComponent
noteReactNode
variant`muted` and `card` sit inside the container; `primary` inverts the band."muted" | "card" | "primary" | "plain""muted"
layout`center` stacks everything; `split` puts the buttons beside the copy."center" | "split""center"
headingLevelHeadingLevel2
as"section" | "div""section"
spacing"sm" | "md" | "lg" | "none""md"
classNamestring
idstring