Vue componentFree

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

Install
Full screen ↗

About this component

The workhorse section of every product page: a short heading and intro, then a grid of features, each with an icon, a title and one or two sentences of explanation.

It ships with ten matching stroke icons (bolt, shield, chart, sync, users, code, globe, bell, lock, sparkles) drawn as inline SVG, so there is no icon package to install. Need a different one? The scoped icon slot receives each feature and its index.

Pick variant="plain" for an airy, open layout or variant="cards" for bordered tiles, and set columns to 2, 3 or 4. The grid collapses to one column on phones and two on tablets.

  • 10 built-in inline SVG icons
  • Plain and card variants
  • 2, 3 or 4 column layouts
  • Scoped icon slot and footer slot
  • Light and dark mode

Code

11 files in the repo
<script setup lang="ts">
import { computed } from 'vue'

/** Built-in icon names. Use the `icon` slot for anything else. */
export type FeatureIcon = 'bolt' | 'shield' | 'chart' | 'sync' | 'users' | 'code' | 'globe' | 'bell' | 'lock' | 'sparkles'

export interface Feature {
  title: string
  description: string
  icon?: FeatureIcon
}

const props = withDefaults(defineProps<{
  eyebrow?: string
  heading?: string
  description?: string
  features?: Feature[]
  /** Columns at the widest breakpoint. */
  columns?: 2 | 3 | 4
  /** 'plain' puts icons above text; 'cards' wraps each feature in a bordered card. */
  variant?: 'plain' | 'cards'
}>(), {
  eyebrow: 'Why Ledgerline',
  heading: 'Bookkeeping that keeps up with you',
  description: 'Everything a small business needs to stay on top of its money, without an accounting degree or a spreadsheet held together with tape.',
  columns: 3,
  variant: 'plain',
  features: () => [
    { icon: 'sync', title: 'Automatic bank feeds', description: 'Connect your accounts once and every transaction lands in Ledgerline within minutes, already categorised.' },
    { icon: 'sparkles', title: 'Smart reconciliation', description: 'Matches receipts to payments for you and flags only the handful that need a human look.' },
    { icon: 'chart', title: 'Live cash-flow forecast', description: 'See the next 90 days of cash at a glance, including upcoming invoices and recurring bills.' },
    { icon: 'bell', title: 'Invoice reminders', description: 'Polite, scheduled nudges go out on your behalf, so you get paid without chasing.' },
    { icon: 'users', title: 'Invite your accountant', description: 'Give your bookkeeper their own login with exactly the access they need. No password sharing.' },
    { icon: 'shield', title: 'Bank-grade security', description: 'Read-only bank connections, encrypted storage and two-factor sign-in on every account.' },
  ],
})

defineSlots<{
  icon?: (props: { feature: Feature, index: number }) => unknown
  footer?: () => unknown
}>()

const gridCols = computed(() => ({
  2: 'sm:grid-cols-2',
  3: 'sm:grid-cols-2 lg:grid-cols-3',
  4: 'sm:grid-cols-2 lg:grid-cols-4',
}[props.columns]))

// 24x24 stroke icons, drawn to share one visual weight.
const icons: Record<FeatureIcon, string[]> = {
  bolt: ['M13 2 4 14h7l-1 8 9-12h-7l1-8Z'],
  shield: ['M12 3 5 6v5c0 4.5 3 8.3 7 10 4-1.7 7-5.5 7-10V6l-7-3Z', 'm9 12 2 2 4-4'],
  chart: ['M4 20V10', 'M10 20V4', 'M16 20v-7', 'M22 20H2'],
  sync: ['M20 11a8 8 0 0 0-14.3-4.9L4 8', 'M4 3v5h5', 'M4 13a8 8 0 0 0 14.3 4.9L20 16', 'M20 21v-5h-5'],
  users: ['M16 20v-1.5a3.5 3.5 0 0 0-3.5-3.5h-5A3.5 3.5 0 0 0 4 18.5V20', 'M10 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z', 'M20 20v-1.5a3.5 3.5 0 0 0-2.5-3.35', 'M15.5 4.15a3.5 3.5 0 0 1 0 6.7'],
  code: ['m8 7-5 5 5 5', 'm16 7 5 5-5 5', 'm14 4-4 16'],
  globe: ['M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Z', 'M3 12h18', 'M12 3c2.5 2.7 3.8 5.7 3.8 9s-1.3 6.3-3.8 9c-2.5-2.7-3.8-5.7-3.8-9S9.5 5.7 12 3Z'],
  bell: ['M6 9a6 6 0 1 1 12 0c0 5 2 6.5 2 6.5H4S6 14 6 9Z', 'M10 19a2 2 0 0 0 4 0'],
  lock: ['M6 11h12v10H6z', 'M8.5 11V7.5a3.5 3.5 0 0 1 7 0V11'],
  sparkles: ['M10 3 11.8 8.2 17 10l-5.2 1.8L10 17l-1.8-5.2L3 10l5.2-1.8L10 3Z', 'M18 14l.9 2.1L21 17l-2.1.9L18 20l-.9-2.1L15 17l2.1-.9L18 14Z'],
}
</script>

<template>
  <section class="px-6 py-20 sm:py-24 lg:px-8">
    <div class="mx-auto max-w-2xl text-center">
      <p v-if="eyebrow" class="text-sm font-semibold text-teal-700 dark:text-teal-400">{{ eyebrow }}</p>
      <h2 class="mt-2 text-3xl font-bold tracking-tight text-balance text-gray-900 sm:text-4xl dark:text-white">{{ heading }}</h2>
      <p v-if="description" class="mt-4 text-lg text-pretty text-gray-600 dark:text-gray-400">{{ description }}</p>
    </div>

    <ul class="mx-auto mt-16 grid max-w-6xl grid-cols-1" :class="[gridCols, variant === 'cards' ? 'gap-6' : 'gap-x-10 gap-y-12']">
      <li
        v-for="(feature, index) in features"
        :key="feature.title"
        :class="variant === 'cards' ? 'rounded-2xl border border-gray-200 bg-white p-6 transition hover:border-teal-300 hover:shadow-sm dark:border-gray-800 dark:bg-gray-900 dark:hover:border-teal-700' : ''"
      >
        <div class="grid size-11 place-items-center rounded-xl bg-teal-50 text-teal-700 ring-1 ring-teal-600/15 ring-inset dark:bg-teal-400/10 dark:text-teal-300 dark:ring-teal-400/20">
          <slot name="icon" :feature="feature" :index="index">
            <svg v-if="feature.icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="size-5">
              <path v-for="d in icons[feature.icon]" :key="d" :d="d" />
            </svg>
          </slot>
        </div>
        <h3 class="mt-5 text-base font-semibold text-gray-900 dark:text-white">{{ feature.title }}</h3>
        <p class="mt-2 text-sm leading-6 text-gray-600 dark:text-gray-400">{{ feature.description }}</p>
      </li>
    </ul>

    <div v-if="$slots.footer" class="mx-auto mt-16 max-w-6xl text-center">
      <slot name="footer" />
    </div>
  </section>
</template>

Props

PropTypeDefaultDescription
eyebrowstring'Why Ledgerline'Small accent label above the heading.
headingstring'Bookkeeping that keeps up with you'Section heading.
descriptionstring'Everything a small business needs…'Intro paragraph. Empty string hides it.
featuresFeature[]6 bookkeeping featuresItems with title, description and an optional built-in icon name.
columns2 | 3 | 43Number of columns at the widest breakpoint.
variant'plain' | 'cards''plain'Open layout or bordered cards.

Slots

SlotDescription
iconCustom icon per feature. Receives { feature, index }.
footerContent centred under the grid, e.g. a link to all features.
All feature sections →
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
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 Logo CloudFree

Component · Feature sections

Logo Cloud

"Trusted by" logo cloud for Vue 3 with a bordered grid or an endless marquee, using text wordmarks and SVG marks. Tailwind CSS, dark mode, reduced motion.

by Vueplay
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

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