Vue componentFree

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 Vueplayv1.0.0Updated 2 hours ago1 installs

Install
Full screen ↗

About this component

The classic SaaS hero: a clear headline with an accent phrase, a short supporting paragraph, a primary and secondary call to action, and a product visual that shows what the app actually looks like.

The product window is built from plain divs and Tailwind utilities rather than a screenshot, so it stays sharp at every size, follows light and dark mode, and weighs nothing. Swap it for your own screenshot or illustration through the visual slot.

Every piece of copy is a prop. Buttons are real links and also emit an action event, so you can route, open a modal or track analytics without forking the component.

  • CSS-drawn app window mockup
  • Primary and secondary CTAs with action event
  • Heading and visual slots
  • Stacks cleanly on mobile
  • Light and dark mode

Code

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

withDefaults(defineProps<{
  eyebrow?: string
  heading?: string
  highlight?: string
  description?: string
  primaryAction?: HeroAction
  secondaryAction?: HeroAction
  /** Short reassurance lines shown under the buttons. */
  notes?: string[]
  /** Name shown in the mocked app window's title bar. */
  appName?: string
}>(), {
  eyebrow: 'New · Shared inboxes for support teams',
  heading: 'Answer every customer',
  highlight: 'without the chaos',
  description: 'Relaybird pulls email, chat and forms into one calm queue. Assign in a click, see who is replying, and never send the same answer twice.',
  primaryAction: () => ({ label: 'Start free trial', href: '#signup' }),
  secondaryAction: () => ({ label: 'Book a demo', href: '#demo' }),
  notes: () => ['14-day trial', 'No credit card', 'Cancel anytime'],
  appName: 'Relaybird',
})

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

const conversations = [
  { initials: 'MA', name: 'Mara Alvik', subject: 'Invoice shows the wrong VAT number', time: '2m', tag: 'Billing', unread: true },
  { initials: 'TO', name: 'Tomas Okafor', subject: 'Can we add a second workspace?', time: '14m', tag: 'Sales', unread: true },
  { initials: 'LK', name: 'Lena Kraus', subject: 'Export to CSV is timing out', time: '1h', tag: 'Bug', unread: false },
  { initials: 'JR', name: 'Jun Reyes', subject: 'Thanks, that fixed it!', time: '3h', tag: 'Done', unread: false },
]
</script>

<template>
  <section class="relative overflow-hidden">
    <div aria-hidden="true" class="pointer-events-none absolute -top-40 right-0 -z-0 size-[40rem] rounded-full bg-indigo-400/20 blur-3xl dark:bg-indigo-500/10" />
    <div class="relative mx-auto grid max-w-7xl items-center gap-14 px-6 py-20 sm:py-24 lg:grid-cols-2 lg:gap-12 lg:px-8 lg:py-32">
      <div class="max-w-xl">
        <p v-if="eyebrow" class="inline-flex items-center gap-2 rounded-full bg-indigo-50 px-3 py-1 text-sm font-medium text-indigo-700 ring-1 ring-indigo-600/15 ring-inset dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-400/20">
          <span class="size-1.5 rounded-full bg-indigo-600 dark:bg-indigo-400" aria-hidden="true" />
          {{ eyebrow }}
        </p>
        <h1 class="mt-6 text-4xl font-bold tracking-tight text-balance text-gray-900 sm:text-5xl lg:text-6xl dark:text-white">
          <slot name="heading">
            {{ heading }}
            <span v-if="highlight" class="text-indigo-600 dark:text-indigo-400">{{ highlight }}</span>
          </slot>
        </h1>
        <p class="mt-6 text-lg leading-8 text-pretty text-gray-600 dark:text-gray-300">{{ description }}</p>
        <div class="mt-10 flex flex-col gap-3 sm:flex-row sm:items-center">
          <a
            :href="primaryAction.href"
            class="inline-flex items-center justify-center gap-2 rounded-xl bg-indigo-600 px-5 py-3 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-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="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" 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-gray-900 ring-1 ring-gray-300 transition ring-inset hover:bg-gray-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:text-white dark:ring-gray-700 dark:hover:bg-gray-800"
            @click="emit('action', 'secondary', $event)"
          >
            {{ secondaryAction.label }}
          </a>
        </div>
        <ul v-if="notes.length" class="mt-8 flex flex-wrap gap-x-6 gap-y-2 text-sm text-gray-500 dark:text-gray-400">
          <li v-for="note in notes" :key="note" class="flex items-center gap-2">
            <svg viewBox="0 0 20 20" aria-hidden="true" class="size-4 fill-indigo-600 dark:fill-indigo-400"><path fill-rule="evenodd" d="M16.7 5.3a1 1 0 0 1 0 1.4l-8 8a1 1 0 0 1-1.4 0l-4-4a1 1 0 1 1 1.4-1.4L8 12.6l7.3-7.3a1 1 0 0 1 1.4 0Z" clip-rule="evenodd" /></svg>
            {{ note }}
          </li>
        </ul>
      </div>

      <!-- Product mock: drawn entirely with divs so it scales and themes with the page. -->
      <div class="relative">
        <slot name="visual">
          <div role="img" :aria-label="`Screenshot of the ${appName} shared inbox`" class="relative rounded-2xl bg-gray-900/5 p-2 ring-1 ring-gray-900/10 dark:bg-white/5 dark:ring-white/10">
            <div class="overflow-hidden rounded-xl bg-white shadow-2xl shadow-indigo-900/10 ring-1 ring-gray-900/5 dark:bg-gray-900 dark:shadow-black/40 dark:ring-white/10">
              <div class="flex items-center gap-2 border-b border-gray-100 px-4 py-3 dark:border-gray-800">
                <span class="size-3 rounded-full bg-rose-400" />
                <span class="size-3 rounded-full bg-amber-400" />
                <span class="size-3 rounded-full bg-emerald-400" />
                <span class="ml-3 hidden rounded-md bg-gray-100 px-3 py-1 text-xs text-gray-500 sm:block dark:bg-gray-800 dark:text-gray-400">app.{{ appName.toLowerCase() }}.example/inbox</span>
              </div>
              <div class="grid grid-cols-[3.5rem_1fr] sm:grid-cols-[9rem_1fr]">
                <div class="space-y-1 border-r border-gray-100 p-2 sm:p-3 dark:border-gray-800">
                  <div class="mb-3 flex items-center gap-2 px-1">
                    <span class="grid size-7 place-items-center rounded-lg bg-indigo-600 text-xs font-bold text-white">{{ appName.charAt(0) }}</span>
                    <span class="hidden text-sm font-semibold text-gray-900 sm:block dark:text-white">{{ appName }}</span>
                  </div>
                  <div v-for="(item, i) in ['Inbox', 'Mine', 'Snoozed', 'Closed']" :key="item" class="flex items-center gap-2 rounded-md px-2 py-1.5 text-xs" :class="i === 0 ? 'bg-indigo-50 font-medium text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300' : 'text-gray-500 dark:text-gray-400'">
                    <span class="size-2 shrink-0 rounded-sm" :class="i === 0 ? 'bg-indigo-500' : 'bg-gray-300 dark:bg-gray-600'" />
                    <span class="hidden sm:inline">{{ item }}</span>
                    <span v-if="i === 0" class="ml-auto hidden rounded bg-indigo-600 px-1.5 text-[10px] text-white sm:inline">12</span>
                  </div>
                </div>
                <ul class="divide-y divide-gray-100 dark:divide-gray-800">
                  <li v-for="c in conversations" :key="c.name" class="flex items-start gap-3 px-3 py-3 sm:px-4" :class="c.unread ? '' : 'opacity-70'">
                    <span class="grid size-8 shrink-0 place-items-center rounded-full bg-gray-100 text-[11px] font-semibold text-gray-700 dark:bg-gray-800 dark:text-gray-200">{{ c.initials }}</span>
                    <div class="min-w-0 flex-1">
                      <div class="flex items-center gap-2">
                        <span class="truncate text-xs font-semibold text-gray-900 dark:text-white">{{ c.name }}</span>
                        <span v-if="c.unread" class="size-1.5 rounded-full bg-indigo-500" />
                        <span class="ml-auto text-[11px] text-gray-400">{{ c.time }}</span>
                      </div>
                      <p class="mt-0.5 truncate text-xs text-gray-500 dark:text-gray-400">{{ c.subject }}</p>
                    </div>
                    <span class="hidden rounded-full bg-gray-100 px-2 py-0.5 text-[10px] font-medium text-gray-600 sm:inline dark:bg-gray-800 dark:text-gray-300">{{ c.tag }}</span>
                  </li>
                </ul>
              </div>
            </div>
            <!-- Floating toast -->
            <div class="absolute -bottom-10 left-3 flex items-center sm:-bottom-6 gap-3 rounded-xl bg-white px-4 py-3 shadow-lg ring-1 ring-gray-900/5 sm:-left-8 dark:bg-gray-800 dark:ring-white/10">
              <span class="grid size-8 place-items-center rounded-full bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-400">
                <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-4"><path fill-rule="evenodd" d="M16.7 5.3a1 1 0 0 1 0 1.4l-8 8a1 1 0 0 1-1.4 0l-4-4a1 1 0 1 1 1.4-1.4L8 12.6l7.3-7.3a1 1 0 0 1 1.4 0Z" clip-rule="evenodd" /></svg>
              </span>
              <div>
                <p class="text-xs font-semibold text-gray-900 dark:text-white">Median first reply</p>
                <p class="text-xs text-gray-500 dark:text-gray-400">4 min · down 38% this week</p>
              </div>
            </div>
          </div>
        </slot>
      </div>
    </div>
  </section>
</template>

Props

PropTypeDefaultDescription
eyebrowstring'New · Shared inboxes for support teams'Small pill above the heading. Pass an empty string to hide it.
headingstring'Answer every customer'Main headline.
highlightstring'without the chaos'Accent-coloured phrase appended to the heading.
descriptionstring'Relaybird pulls email, chat and forms…'Supporting paragraph under the heading.
primaryActionHeroAction{ label: 'Start free trial', href: '#signup' }Primary button label and link.
secondaryActionHeroAction{ label: 'Book a demo', href: '#demo' }Secondary button label and link.
notesstring[]['14-day trial', 'No credit card', 'Cancel anytime']Short reassurance lines with check marks under the buttons.
appNamestring'Relaybird'Product name shown inside the mocked app window.

Events

EventPayloadDescription
action(kind: 'primary' | 'secondary', event: MouseEvent)Fired when either CTA is clicked. Call event.preventDefault() to handle navigation yourself.

Slots

SlotDescription
headingReplaces the heading and highlight text inside the h1.
visualReplaces the drawn app window on the right, e.g. with a real screenshot.
All hero sections →
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
Preview of Hero Email CaptureFree

Component · Hero sections

Hero Email Capture

Vue 3 waitlist hero with an accessible email form: inline validation, loading and success states, and a typed submit event. Tailwind CSS, dark mode.

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 Bento GridFree

Component · Feature sections

Bento Grid

Bento-style Vue 3 feature grid with wide and tall tiles and small CSS-drawn visuals like maps, timelines and code. Tailwind CSS, dark mode, typed tiles.

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