Vue componentFree

Testimonial Grid

Masonry grid of testimonial cards for Vue 3 with initials avatars, optional star ratings and highlighted quotes. Tailwind CSS, dark mode.

by Vueplayv1.0.0Updated 3 hours ago

Install
Full screen ↗

About this component

A social proof section that fits quotes of any length. Cards flow into one, two or three masonry columns using CSS columns, so a two-line quote and a paragraph sit together without awkward gaps.

Each card shows the quote, the person's name, role and company, and an initials avatar tinted from a stable hash of the name, so you don't need headshots. Mark one or two quotes as featured to give them a warm background and larger type. Star ratings are optional per card and announced as "Rated 4 out of 5".

Pass your own testimonials array, turn ratings off with showRating, and use the footer slot for a link to case studies.

  • CSS-columns masonry layout
  • Initials avatars with stable tints
  • Optional accessible star ratings
  • Featured quote styling
  • Light and dark mode

Code

11 files in the repo
<script setup lang="ts">
export interface Testimonial {
  quote: string
  name: string
  role: string
  company: string
  /** 1 to 5. Omit to hide the stars on this card. */
  rating?: number
  /** Highlighted card with larger text. Use for one or two standout quotes. */
  featured?: boolean
}

withDefaults(defineProps<{
  eyebrow?: string
  heading?: string
  subheading?: string
  testimonials?: Testimonial[]
  /** Show star ratings on cards that have one. */
  showRating?: boolean
}>(), {
  eyebrow: 'Loved by busy teams',
  heading: 'Less chasing, more shipping',
  subheading: 'Over 2,400 teams moved their weekly planning to Tidewell. Here is what a few of them noticed first.',
  showRating: true,
  testimonials: () => [
    { quote: 'We replaced three spreadsheets and a standing meeting with one board. Our Monday planning went from an hour to fifteen minutes, and nobody misses the old way.', name: 'Maren Okafor', role: 'Head of Product', company: 'Fieldnote', rating: 5, featured: true },
    { quote: 'Setup took an afternoon. The import mapped our old statuses almost perfectly.', name: 'Tomas Reyes', role: 'Engineering Manager', company: 'Brightloop', rating: 5 },
    { quote: 'The weekly digest is the only status email my leadership team actually reads.', name: 'Priya Lindqvist', role: 'COO', company: 'Harbor & Pine', rating: 5 },
    { quote: 'I like that it stays out of the way. Keyboard shortcuts for everything, no clutter, and it is fast even on our biggest projects with thousands of tasks.', name: 'Jonah Whitfield', role: 'Staff Engineer', company: 'Quillstack', rating: 4 },
    { quote: 'Our clients get a read-only view of progress, so the "any update?" emails stopped almost overnight.', name: 'Aiko Brandt', role: 'Studio Lead', company: 'Northpaw Design', rating: 5 },
    { quote: 'Support answered a tricky permissions question in twenty minutes, on a Sunday. That sold me.', name: 'Samuel Adeyemi', role: 'IT Lead', company: 'Coralfin Health', rating: 5 },
    { quote: 'Finally a planning tool that the design team and the backend team both like using. That alone was worth the switch.', name: 'Elena Vasquez', role: 'VP Engineering', company: 'Lanternworks', rating: 5, featured: true },
  ],
})

const initials = (name: string) => name.split(/\s+/).filter(Boolean).slice(0, 2).map(p => p[0]!.toUpperCase()).join('')

// A stable tint per person so avatars feel varied without images.
const tints = [
  'bg-amber-100 text-amber-800 dark:bg-amber-400/15 dark:text-amber-200',
  'bg-sky-100 text-sky-800 dark:bg-sky-400/15 dark:text-sky-200',
  'bg-emerald-100 text-emerald-800 dark:bg-emerald-400/15 dark:text-emerald-200',
  'bg-rose-100 text-rose-800 dark:bg-rose-400/15 dark:text-rose-200',
  'bg-violet-100 text-violet-800 dark:bg-violet-400/15 dark:text-violet-200',
]
function tint(name: string) {
  let h = 0
  for (const ch of name) h = (h * 31 + ch.charCodeAt(0)) >>> 0
  return tints[h % tints.length]
}
</script>

<template>
  <section class="px-4 py-20 sm:px-6 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-amber-700 dark:text-amber-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 class="mt-4 text-lg text-pretty text-gray-600 dark:text-gray-400">{{ subheading }}</p>
    </div>

    <ul class="mx-auto mt-14 max-w-6xl columns-1 gap-6 sm:columns-2 lg:columns-3" role="list">
      <li v-for="t in testimonials" :key="t.name" class="mb-6 break-inside-avoid">
        <figure
          class="flex flex-col rounded-2xl p-6 ring-1 sm:p-7"
          :class="t.featured
            ? 'bg-amber-50 ring-amber-200 dark:bg-amber-400/[0.06] dark:ring-amber-400/20'
            : 'bg-white shadow-sm ring-gray-200 dark:bg-gray-900 dark:shadow-none dark:ring-white/10'"
        >
          <div v-if="showRating && t.rating" class="mb-4 flex gap-0.5" role="img" :aria-label="`Rated ${t.rating} out of 5`">
            <svg
              v-for="n in 5"
              :key="n"
              viewBox="0 0 20 20"
              aria-hidden="true"
              class="size-4.5"
              :class="n <= t.rating ? 'fill-amber-400' : 'fill-gray-200 dark:fill-white/15'"
            >
              <path d="M10 1.8l2.47 5.01 5.53.8-4 3.9.94 5.5L10 14.42 5.06 17l.94-5.5-4-3.9 5.53-.8L10 1.8Z" />
            </svg>
          </div>
          <blockquote
            class="text-pretty text-gray-800 dark:text-gray-200"
            :class="t.featured ? 'text-lg/8 font-medium' : 'text-base/7'"
          >
            <p>&ldquo;{{ t.quote }}&rdquo;</p>
          </blockquote>
          <figcaption class="mt-6 flex items-center gap-3">
            <span class="grid size-10 shrink-0 place-items-center rounded-full text-sm font-semibold" :class="tint(t.name)" aria-hidden="true">
              {{ initials(t.name) }}
            </span>
            <span class="min-w-0">
              <span class="block text-sm font-semibold text-gray-900 dark:text-white">{{ t.name }}</span>
              <span class="block text-sm text-gray-600 dark:text-gray-400">{{ t.role }}, {{ t.company }}</span>
            </span>
          </figcaption>
        </figure>
      </li>
    </ul>

    <div v-if="$slots.footer" class="mt-8 text-center">
      <slot name="footer" />
    </div>
  </section>
</template>

Props

PropTypeDefaultDescription
eyebrowstring'Loved by busy teams'Small label above the heading. Empty string hides it.
headingstring'Less chasing, more shipping'Section heading.
subheadingstring'Over 2,400 teams…'Supporting text under the heading.
testimonialsTestimonial[]7 sample quotesQuotes with name, role, company, optional rating (1-5) and optional featured flag.
showRatingbooleantrueShow star ratings on cards that have one.

Slots

SlotDescription
footerContent centred below the grid, e.g. a link to case studies.
All testimonials →
Preview of Testimonial CarouselFree

Component · Testimonials

Testimonial Carousel

Accessible Vue 3 testimonial carousel with prev/next, dots and autoplay that pauses on hover or focus and honours reduced motion. Tailwind CSS.

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