# CTA banner

The simplest ask: one banner with a gradient hairline around a soft tint, the promise on one side and two actions on the other.

Docs: https://dashral.com/blocks/cta-banner

## When to use

- The simplest ask: one banner with the promise on one side and two actions on the other.
- The end of a page or between sections, where one more nudge helps.

## When not to use

- The last section of a landing page, where one ask should stand out: CTA centered.
- Asks that begin with an email: CTA with sign-up.

## Installation

```bash
npx shadcn@latest add https://dashral.com/r/cta-banner.json
```

## Usage

Every prop has the sample as its default, so pass only what you have.

```tsx
import { CtaBanner } from "@/components/dashral/blocks/cta-banner"

<CtaBanner
  title="Ready when you are"
  description="Free for 14 days. No card needed."
  primary={{ label: "Start free", href: "/register" }}
  secondary={{ label: "Talk to sales", href: "/contact" }}
/>
```

## API reference

### CtaBanner

The simplest call to action: one banner, the ask on one side and the actions on the other. A gradient hairline around a soft tint of the same colors, so it stands out from the page without shouting. On phones the two stack, the main action first. Every part comes from props.

| Prop | Type | Default | Description |
|---|---|---|---|
| `eyebrow` | `React.ReactNode` | `"Free for small teams"` |  |
| `title` | `React.ReactNode` | `sample` |  |
| `description` | `React.ReactNode` | `sample` |  |
| `primary` | `BlockAction` | `sample` |  |
| `secondary` | `BlockAction \| null` | `sample` | The second button; null hides it. |

### type BlockAction

A button: a link with href, or your handler with onClick.

| Prop | Type | Default | Description |
|---|---|---|---|
| `label` | `string` | — |  |
| `href` | `string` | — |  |
| `onClick` | `() => void` | — |  |

## Source

```tsx
import { ArrowRight, Sparkles } from "lucide-react"
import { Button } from "@/components/dashral/button"

/** A button: a link with href, or your handler with onClick. */
export type BlockAction = { label: string; href?: string; onClick?: () => void }

export type CtaBannerProps = {
  eyebrow?: React.ReactNode
  title?: React.ReactNode
  description?: React.ReactNode
  primary?: BlockAction
  /** The second button; null hides it. */
  secondary?: BlockAction | null
}

/**
 * The simplest call to action: one banner, the ask on one side and the actions on the other. A gradient hairline
 * around a soft tint of the same colors, so it stands out from the page without shouting. On phones the two stack,
 * the main action first. Every part comes from props.
 */
export function CtaBanner({
  eyebrow = "Free for small teams",
  title = "Ready to see your numbers in one place?",
  description = "Start free in a minute. No credit card, and your whole team is invited.",
  primary = { label: "Start for free", href: "#" },
  secondary = { label: "Talk to sales", href: "#" },
}: CtaBannerProps) {
  return (
    <section className="bg-background p-4 sm:p-6">
      <div
        className={`relative isolate mx-auto flex flex-col gap-6 overflow-hidden rounded-2xl border border-transparent px-6 py-8 sm:px-10 sm:py-10 md:flex-row md:items-center md:justify-between [background:linear-gradient(120deg,color-mix(in_oklab,var(--gradient-from)_18%,var(--card)),color-mix(in_oklab,var(--gradient-via)_10%,var(--card))_55%,color-mix(in_oklab,var(--gradient-to)_18%,var(--card)))_padding-box,linear-gradient(90deg,var(--gradient-from),var(--gradient-via),var(--gradient-to))_border-box]`}
      >
        {/* A glow in the middle color from one corner and a fine grid fading out, both decoration. */}
        <div aria-hidden className="absolute -top-28 -right-20 -z-10 size-80 rounded-full bg-(--gradient-via)/20 blur-3xl rtl:right-auto rtl:-left-20" />
        <div
          aria-hidden
          className="absolute inset-0 -z-10 [background-image:linear-gradient(to_right,var(--gradient-via)_1px,transparent_1px),linear-gradient(to_bottom,var(--gradient-via)_1px,transparent_1px)] [background-size:32px_32px] opacity-[0.08] [mask-image:linear-gradient(to_left,black,transparent_65%)] rtl:[mask-image:linear-gradient(to_right,black,transparent_65%)]"
        />

        <div className="max-w-xl">
          {eyebrow && (
            <p className="flex items-center gap-1.5 text-sm font-medium text-(--gradient-ink)">
              <Sparkles aria-hidden className="size-4" />
              {eyebrow}
            </p>
          )}
          <h2 className="mt-2 text-2xl font-semibold tracking-tight text-balance sm:text-3xl">{title}</h2>
          {description && <p className="mt-2 text-pretty text-muted-foreground">{description}</p>}
        </div>

        <div className="flex shrink-0 flex-col gap-3 sm:flex-row">
          <Action action={primary} className="group">
            {primary.label}
            <ArrowRight
              aria-hidden
              className="transition-transform duration-(--duration-fast) group-hover:translate-x-0.5 rtl:-scale-x-100 rtl:group-hover:-translate-x-0.5"
            />
          </Action>
          {secondary && (
            <Action action={secondary} variant="outline">
              {secondary.label}
            </Action>
          )}
        </div>
      </div>
    </section>
  )
}

/** A large button that follows its action: a link, or a handler. */
function Action({ action, variant, className, children }: { action: BlockAction; variant?: "outline"; className?: string; children: React.ReactNode }) {
  return action.href ? (
    <Button size="lg" variant={variant} className={className} asChild>
      <a href={action.href}>{children}</a>
    </Button>
  ) : (
    <Button size="lg" variant={variant} className={className} onClick={action.onClick}>
      {children}
    </Button>
  )
}
```