Vue componentFree

CTA Banner

Vue 3 call-to-action banner in three layouts (glowing panel, split card and slim band) with primary and secondary buttons. Tailwind CSS, dark mode.

by Vueplayv1.0.0Updated 2 hours ago

Install
Full screen ↗

About this component

Every landing page needs a closing argument. This component gives you three: a dark panel with a soft glow for the end of the page, a split card with text on the left and buttons on the right for mid-page prompts, and a slim band for announcements between sections.

All three share the same props (heading, description, primary and secondary action) so you can switch layouts with a single variant change. Set secondaryAction to null to show just one button, use the heading slot for rich text, and the default slot for extra content like a trust line.

Buttons are regular links that also emit an action event, handy for analytics or opening a signup modal instead of navigating.

  • Three layouts via one variant prop
  • Primary and optional secondary action
  • Emits action for tracking or modals
  • Stacks cleanly on mobile
  • Light and dark mode

Code

11 files in the repo
<script setup lang="ts">
export interface CtaLink {
  label: string
  href: string
}

withDefaults(defineProps<{
  /**
   * 'panel'  – centred text in a dark, glowing panel
   * 'split'  – text left, buttons right, on a light accent panel
   * 'band'   – slim full-width strip, good between sections
   */
  variant?: 'panel' | 'split' | 'band'
  heading?: string
  description?: string
  primaryAction?: CtaLink
  secondaryAction?: CtaLink | null
}>(), {
  variant: 'panel',
  heading: 'Ready to close your books in a day?',
  description: 'Start a free 30-day trial of Tallyhouse. Import last year’s data in minutes and see how much time month-end really takes.',
  primaryAction: () => ({ label: 'Start free trial', href: '#trial' }),
  secondaryAction: () => ({ label: 'Talk to sales', href: '#sales' }),
})

const emit = defineEmits<{
  action: [kind: 'primary' | 'secondary', event: MouseEvent]
}>()

const arrow = 'M3 10a.75.75 0 0 1 .75-.75h10.64l-3.22-3.22a.75.75 0 1 1 1.06-1.06l4.5 4.5a.75.75 0 0 1 0 1.06l-4.5 4.5a.75.75 0 1 1-1.06-1.06l3.22-3.22H3.75A.75.75 0 0 1 3 10Z'
</script>

<template>
  <!-- Panel -->
  <section v-if="variant === 'panel'" class="px-6 py-16 sm:py-20 lg:px-8">
    <div class="relative isolate mx-auto max-w-5xl overflow-hidden rounded-3xl bg-gray-900 px-6 py-16 text-center shadow-2xl sm:px-16 sm:py-20 dark:bg-gray-900 dark:ring-1 dark:ring-white/10">
      <div aria-hidden="true" class="absolute -top-24 left-1/2 -z-10 h-64 w-[36rem] max-w-none -translate-x-1/2 rounded-full bg-orange-500/40 blur-3xl" />
      <div aria-hidden="true" class="absolute inset-0 -z-10 bg-[linear-gradient(to_right,rgb(255_255_255/0.05)_1px,transparent_1px),linear-gradient(to_bottom,rgb(255_255_255/0.05)_1px,transparent_1px)] mask-[radial-gradient(ellipse_at_top,black,transparent_70%)] bg-size-[40px_40px]" />
      <h2 class="mx-auto max-w-2xl text-3xl font-bold tracking-tight text-balance text-white sm:text-4xl">
        <slot name="heading">{{ heading }}</slot>
      </h2>
      <p v-if="description" class="mx-auto mt-5 max-w-xl text-lg leading-8 text-pretty text-gray-300">{{ description }}</p>
      <div class="mt-10 flex flex-col items-stretch justify-center gap-3 sm:flex-row sm:items-center">
        <a :href="primaryAction.href" class="inline-flex items-center justify-center gap-2 rounded-xl bg-orange-500 px-5 py-3 text-sm font-semibold text-gray-950 shadow-sm transition hover:bg-orange-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-400" @click="emit('action', 'primary', $event)">
          {{ primaryAction.label }}
          <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-4"><path fill-rule="evenodd" :d="arrow" clip-rule="evenodd" /></svg>
        </a>
        <a v-if="secondaryAction" :href="secondaryAction.href" class="inline-flex items-center justify-center rounded-xl px-5 py-3 text-sm font-semibold text-white ring-1 ring-white/20 transition ring-inset hover:bg-white/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white" @click="emit('action', 'secondary', $event)">
          {{ secondaryAction.label }}
        </a>
      </div>
      <slot />
    </div>
  </section>

  <!-- Split -->
  <section v-else-if="variant === 'split'" class="px-6 py-16 sm:py-20 lg:px-8">
    <div class="mx-auto flex max-w-6xl flex-col gap-8 rounded-3xl bg-orange-50 px-6 py-10 ring-1 ring-orange-900/10 sm:px-10 lg:flex-row lg:items-center lg:justify-between lg:gap-12 lg:px-14 lg:py-14 dark:bg-orange-500/10 dark:ring-orange-400/20">
      <div class="max-w-2xl">
        <h2 class="text-2xl font-bold tracking-tight text-balance text-gray-900 sm:text-3xl dark:text-white">
          <slot name="heading">{{ heading }}</slot>
        </h2>
        <p v-if="description" class="mt-3 text-base leading-7 text-pretty text-gray-700 dark:text-gray-300">{{ description }}</p>
        <slot />
      </div>
      <div class="flex shrink-0 flex-col gap-3 sm:flex-row">
        <a :href="primaryAction.href" class="inline-flex items-center justify-center gap-2 rounded-xl bg-orange-600 px-5 py-3 text-sm font-semibold text-white shadow-sm transition hover:bg-orange-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600 dark:bg-orange-500 dark:text-gray-950 dark:hover:bg-orange-400" @click="emit('action', 'primary', $event)">
          {{ primaryAction.label }}
          <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-4"><path fill-rule="evenodd" :d="arrow" clip-rule="evenodd" /></svg>
        </a>
        <a v-if="secondaryAction" :href="secondaryAction.href" class="inline-flex items-center justify-center rounded-xl bg-white px-5 py-3 text-sm font-semibold text-gray-900 ring-1 ring-gray-900/10 transition ring-inset hover:bg-gray-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600 dark:bg-gray-900 dark:text-white dark:ring-white/10 dark:hover:bg-gray-800" @click="emit('action', 'secondary', $event)">
          {{ secondaryAction.label }}
        </a>
      </div>
    </div>
  </section>

  <!-- Band -->
  <section v-else class="border-y border-orange-800/20 bg-orange-700 dark:border-orange-400/20 dark:bg-orange-500/15">
    <div class="mx-auto flex max-w-7xl flex-col gap-4 px-6 py-6 sm:flex-row sm:items-center sm:justify-between lg:px-8">
      <p class="text-base font-semibold text-white dark:text-orange-100">
        <slot name="heading">{{ heading }}</slot>
        <span v-if="description" class="mt-1 block text-sm font-normal text-orange-100 sm:mt-0 sm:ml-2 sm:inline dark:text-orange-200/80">{{ description }}</span>
      </p>
      <div class="flex shrink-0 items-center gap-4">
        <a :href="primaryAction.href" class="inline-flex items-center gap-2 rounded-lg bg-white px-4 py-2 text-sm font-semibold text-orange-800 shadow-sm transition hover:bg-orange-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white dark:bg-orange-500 dark:text-gray-950 dark:hover:bg-orange-400" @click="emit('action', 'primary', $event)">
          {{ primaryAction.label }}
        </a>
        <a v-if="secondaryAction" :href="secondaryAction.href" class="text-sm font-semibold text-white underline-offset-4 hover:underline focus-visible:rounded focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white dark:text-orange-100" @click="emit('action', 'secondary', $event)">
          {{ secondaryAction.label }} <span aria-hidden="true">→</span>
        </a>
      </div>
    </div>
  </section>
</template>

Props

PropTypeDefaultDescription
variant'panel' | 'split' | 'band''panel'Layout: dark centred panel, split light card, or slim full-width band.
headingstring'Ready to close your books in a day?'Main line of the banner.
descriptionstring'Start a free 30-day trial of Tallyhouse…'Supporting text. Empty string hides it.
primaryActionCtaLink{ label: 'Start free trial', href: '#trial' }Main button.
secondaryActionCtaLink | null{ label: 'Talk to sales', href: '#sales' }Secondary button or link. Pass null to hide it.

Events

EventPayloadDescription
action(kind: 'primary' | 'secondary', event: MouseEvent)Fired when either button is clicked.

Slots

SlotDescription
headingRich content for the heading.
defaultExtra content under the text (panel and split variants).
All call to action →
Preview of Hero SplitFree

Component · Hero sections

Hero Split

Split Vue 3 hero section with copy and two CTAs on the left and a Tailwind-drawn app window mockup on the right. Dark mode, typed props, no images.

by Vueplay1 installs
Preview of Feature AlternatingFree

Component · Feature sections

Feature Alternating

Alternating Vue 3 feature rows that zigzag text and CSS-drawn illustrations, with bullets and links per row. Tailwind CSS, dark mode, no images needed.

by Vueplay
Preview of Feature GridFree

Component · Feature sections

Feature Grid

Responsive Vue 3 feature grid with a heading, inline SVG icons and plain or card variants in 2, 3 or 4 columns. Tailwind CSS, dark mode, typed data.

by Vueplay
Preview of Hero CenteredFree

Component · Hero sections

Hero Centered

Centered Vue 3 hero with an announcement pill, gradient headline, grid background, two CTAs and a row of wordmarks. Tailwind CSS with dark mode.

by Vueplay

Pro feature

Upgrade to Pro

Upgrade to unlock more of Vueplay. Cancel any time.

  • Custom domains with HTTPS
  • Private projects
  • No "Made with Vueplay" badge
  • GitHub sync

$120/year

Compare plansBilling settings