# CTA Section

> One clear thing to do, in three decisions: a plate that ends the page, text beside a piece of the product, or a slim row inside a long page.

A shadcn/ui block from the [@miseui](https://miseui.junsu4621.workers.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/miseui/cta-section).

- Page, with a live preview: https://shadcn.exchange/r/miseui/cta-section
- Screenshot: https://shadcn.exchange/shot/miseui/cta-section/light.png
- Registry item: https://miseui.junsu4621.workers.dev/r/cta-section.json
- Categories: blocks
- Listed: 2026-11-05, source last changed 2026-11-05

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/miseui/cta-section.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `utils`, `https://miseui.junsu4621.workers.dev/r/badge.json`, `https://miseui.junsu4621.workers.dev/r/button.json`

## Files

### `registry/miseui/blocks/cta-section.tsx`

```tsx
import { cn } from "@/registry/miseui/lib/utils"
import { CtaActions, CtaButton, CtaHeading, CtaNote, DEFAULT_COPY, ROOT_CLASS, type CtaAction, type CtaSectionCopyProps } from "./cta-section-parts"
import { CtaSectionSplit, type CtaSectionSplitProps } from "./cta-section-split"

type CtaSectionVariant = "plate" | "split" | "banner"

interface CtaSectionProps extends CtaSectionSplitProps {
  /** `plate` the last thing on a page, `split` beside a piece of the product, `banner` a slim row inside a long page. Anything else is `plate`. */
  variant?: CtaSectionVariant
}

function CtaSectionPlate({
  headline = DEFAULT_COPY.headline,
  headlineQuiet = DEFAULT_COPY.headlineQuiet,
  description = DEFAULT_COPY.description,
  primaryAction = DEFAULT_COPY.primaryAction,
  secondaryAction = DEFAULT_COPY.secondaryAction,
  note = DEFAULT_COPY.note,
  className,
  ...props
}: CtaSectionCopyProps) {
  return (
    <section data-slot="cta-section" data-variant="plate" className={cn(ROOT_CLASS, className)} {...props}>
      <div className="mx-auto w-full max-w-6xl px-4 py-12 @2xl:px-8 @2xl:py-16">
        <div data-slot="cta-section-plate" className="flex flex-col items-start gap-6 rounded-3xl border bg-background px-6 py-12 dark:bg-card @2xl:items-center @2xl:px-12 @2xl:py-20 @2xl:text-center">
          <CtaHeading headline={headline} quiet={headlineQuiet} className="max-w-2xl @2xl:text-4xl" />
          <p className="max-w-md text-[15px] leading-relaxed text-pretty text-muted-foreground">{description}</p>
          <CtaActions primary={primaryAction} secondary={secondaryAction} className="@2xl:justify-center" />
          <CtaNote note={note} />
        </div>
      </div>
    </section>
  )
}

/** One thing to do and no description: a soft row people can pass, unlike the plate, which asks them to stop. */
function CtaSectionBanner({
  headline = DEFAULT_COPY.headline,
  headlineQuiet = DEFAULT_COPY.headlineQuiet,
  primaryAction = DEFAULT_COPY.primaryAction,
  className,
  ...props
}: Omit<CtaSectionCopyProps, "description" | "secondaryAction" | "note">) {
  return (
    <section data-slot="cta-section" data-variant="banner" className={cn(ROOT_CLASS, className)} {...props}>
      <div className="mx-auto w-full max-w-6xl px-4 py-6 @2xl:px-8">
        {/* Stacks instead of truncating: a clipped sentence beside a button says nothing. */}
        <div data-slot="cta-section-banner" className="flex flex-col items-start gap-3 rounded-2xl bg-mise-soft p-4 dark:border dark:bg-card @2xl:flex-row @2xl:items-center @2xl:justify-between @2xl:gap-6 @2xl:py-3 @2xl:pr-3 @2xl:pl-6">
          <h2 className="min-w-0 text-[15px] leading-snug font-medium text-pretty">
            {headline} <span className="font-normal text-foreground/70">{headlineQuiet}</span>
          </h2>
          <CtaButton action={primaryAction} variant="default" className="h-10 px-4 text-sm" />
        </div>
      </div>
    </section>
  )
}

function CtaSection({ variant, fragment, description, secondaryAction, note, ...props }: CtaSectionProps) {
  switch (variant) {
    case "split":
      return <CtaSectionSplit fragment={fragment} description={description} secondaryAction={secondaryAction} note={note} {...props} />
    case "banner":
      return <CtaSectionBanner {...props} />
    default:
      return <CtaSectionPlate description={description} secondaryAction={secondaryAction} note={note} {...props} />
  }
}

export { CtaSection, type CtaAction, type CtaSectionProps, type CtaSectionVariant }
```

### `registry/miseui/blocks/cta-section-parts.tsx`

```tsx
import type * as React from "react"
import { ArrowRight } from "lucide-react"
import { cn } from "@/registry/miseui/lib/utils"
import { Button, type ButtonProps } from "@/registry/miseui/ui/button"

interface CtaAction {
  label: string
  /** Renders a real link, so it can be opened in a new tab and read by crawlers. */
  href?: string
  onClick?: () => void
}

interface CtaSectionCopyProps extends React.ComponentProps<"section"> {
  /** With headlineQuiet, two lines at most. This is a last nudge and not a second hero. */
  headline?: string
  /** Continues the headline in the same size, one tone quieter. */
  headlineQuiet?: string
  /** One sentence. Not shown on `banner`, which has room for the headline alone. */
  description?: string
  primaryAction?: CtaAction
  /** Null leaves one button. Not shown on `banner`. */
  secondaryAction?: CtaAction | null
  /** What it does not cost, right under the buttons. Say only what is true of your product. Null removes it. */
  note?: string | null
}

const DEFAULT_COPY = {
  headline: "Your next call can end in a plan.",
  headlineQuiet: "Invite it once and see.",
  description: "It listens, writes down who owns what, and sends it to everyone who was there.",
  primaryAction: { label: "Try it on your next call" },
  secondaryAction: { label: "See a sample summary" },
  note: "No card needed. Cancel from settings.",
} as const

const ROOT_CLASS = "@container flex w-full flex-col justify-center bg-muted dark:bg-background"

function CtaButton({ action, variant, className, children }: { action: CtaAction; variant: ButtonProps["variant"]; className?: string; children?: React.ReactNode }) {
  if (action.href) {
    return (
      <Button asChild size="lg" variant={variant} className={className}>
        <a href={action.href} onClick={action.onClick}>
          {action.label}
          {children}
        </a>
      </Button>
    )
  }
  return (
    <Button size="lg" variant={variant} onClick={action.onClick} className={className}>
      {action.label}
      {children}
    </Button>
  )
}

function CtaActions({ primary, secondary, className }: { primary: CtaAction; secondary: CtaAction | null; className?: string }) {
  return (
    <div className={cn("flex flex-wrap gap-3", className)}>
      <CtaButton action={primary} variant="mise">
        <ArrowRight />
      </CtaButton>
      {secondary ? <CtaButton action={secondary} variant="ghost" /> : null}
    </div>
  )
}

function CtaHeading({ headline, quiet, className }: { headline: string; quiet: string; className?: string }) {
  return (
    <h2 className={cn("text-title text-3xl text-balance", className)}>
      {headline} <span className="text-muted-foreground">{quiet}</span>
    </h2>
  )
}

function CtaNote({ note, className }: { note: string | null; className?: string }) {
  if (!note) return null
  return <p className={cn("text-[13px] leading-5 text-pretty text-muted-foreground", className)}>{note}</p>
}

export { CtaActions, CtaButton, CtaHeading, CtaNote, DEFAULT_COPY, ROOT_CLASS, type CtaAction, type CtaSectionCopyProps }
```

### `registry/miseui/blocks/cta-section-split.tsx`

```tsx
import type * as React from "react"
import { cn } from "@/registry/miseui/lib/utils"
import { Badge } from "@/registry/miseui/ui/badge"
import { CtaActions, CtaHeading, CtaNote, DEFAULT_COPY, ROOT_CLASS, type CtaSectionCopyProps } from "./cta-section-parts"

const SAMPLE_ROWS = [
  { owner: "Priya", task: "Send the revised tiers to sales", due: "Thursday" },
  { owner: "Tomas", task: "Draft the customer email", due: "Monday" },
  { owner: "Wen", task: "Brief support on the change", due: "Friday" },
] as const

/** Sits on the bottom edge of the plate, so it reads as something coming out of the product and not a card floating beside the words. */
function CtaSampleFragment() {
  return (
    <figure data-slot="cta-section-fragment" className="m-0 rounded-t-2xl border border-b-0 bg-card dark:bg-muted">
      <figcaption className="flex flex-wrap items-center justify-between gap-2 border-b px-4 py-3">
        <span className="text-[15px] leading-snug">Weekly pricing review</span>
        <Badge variant="secondary" className="font-normal">
          Sample
        </Badge>
      </figcaption>
      <ul className="flex flex-col divide-y">
        {SAMPLE_ROWS.map((row) => (
          <li key={row.owner} className="grid grid-cols-[minmax(0,1fr)_auto] items-baseline gap-x-4 px-4 py-3 text-[13px] leading-5">
            <span className="min-w-0 text-pretty">
              <span className="font-medium">{row.owner}</span> <span className="text-muted-foreground">{row.task}</span>
            </span>
            <span className="whitespace-nowrap text-muted-foreground">{row.due}</span>
          </li>
        ))}
      </ul>
    </figure>
  )
}

interface CtaSectionSplitProps extends CtaSectionCopyProps {
  /** A piece of your real product. Null leaves the text alone on the plate. */
  fragment?: React.ReactNode
}

function CtaSectionSplit({
  headline = DEFAULT_COPY.headline,
  headlineQuiet = DEFAULT_COPY.headlineQuiet,
  description = DEFAULT_COPY.description,
  primaryAction = DEFAULT_COPY.primaryAction,
  secondaryAction = DEFAULT_COPY.secondaryAction,
  note = DEFAULT_COPY.note,
  fragment = <CtaSampleFragment />,
  className,
  ...props
}: CtaSectionSplitProps) {
  const hasFragment = fragment !== null
  return (
    <section data-slot="cta-section" data-variant="split" className={cn(ROOT_CLASS, className)} {...props}>
      <div className="mx-auto w-full max-w-6xl px-4 py-12 @2xl:px-8 @2xl:py-16">
        <div className={cn("grid grid-cols-[minmax(0,1fr)] rounded-3xl border bg-background dark:bg-card", hasFragment && "@4xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]")}>
          <div className="flex min-w-0 flex-col items-start gap-6 p-6 @2xl:p-12">
            <CtaHeading headline={headline} quiet={headlineQuiet} className="@2xl:text-4xl" />
            <p className="max-w-md text-[15px] leading-relaxed text-pretty text-muted-foreground">{description}</p>
            <CtaActions primary={primaryAction} secondary={secondaryAction} />
            <CtaNote note={note} />
          </div>
          {hasFragment ? <div className="flex min-w-0 flex-col justify-end px-6 @2xl:px-12 @4xl:pt-12 @4xl:pr-12 @4xl:pl-0">{fragment}</div> : null}
        </div>
      </div>
    </section>
  )
}

export { CtaSectionSplit, type CtaSectionSplitProps }
```
