Testimonial blocks

Customer quotes for a landing, pricing or product page: a wall of quote cards and a one-at-a-time carousel. Each block is one .svelte file built from SvAvatar, SvRating, SvCarousel and SvButton, themed from the --sg-* tokens. Company marks are made-up glyphs drawn as inline SVG, so nothing loads from another server.

Installation

Add the whole group, or one block by its name (shown under each demo):

See them first with npx @svgrid/ui try blocks/testimonials. No project needed.

Testimonial cards

Six quote cards in CSS columns: three on desktop, two below 1024 px and one below 640 px. Each card has the company wordmark, an optional read-only SvRating, the quote and an SvAvatar with initials, name and role. Quotes over 170 characters are cut at a word and open with Read more. The header shows the average of the rated quotes and a Read customer stories button.

Open the live example: Testimonial cards block (Blocks)

npx @svgrid/ui add testimonials-cards

One large quote at a time in an SvCarousel with arrows, dots and swipe. Autoplay moves every 7 seconds, pauses while the pointer is over the carousel or focus is inside it, and has a pause button. When the reader's system asks for reduced motion there is no autoplay and no slide animation. The wordmark row under the carousel jumps to that company's quote; on phones the arrows move down beside the dots so they do not cover the text.

Open the live example: Testimonial carousel block (Blocks)

npx @svgrid/ui add testimonials-carousel

Feeding your own quotes

Each block's <script module> exports its core: createTestimonialCards() and createTestimonialCarousel(). Both take items and fall back to the sample quotes when you pass none. An item is { id, quote, name, role, company, mark, rating? }, where mark picks the made-up glyph for the wordmark (ring, peak, hex, bars or leaf, plus wave in the cards block). A quote without rating shows no stars and is left out of the cards block's average.

Load the quotes in the route:

// src/routes/+page.server.ts
import { db } from '$lib/server/db'

export const load = async () => ({
  quotes: await db.query.testimonials.findMany({
    where: (t, { eq }) => eq(t.published, true),
    columns: { id: true, quote: true, name: true, role: true, company: true, mark: true, rating: true },
  }),
})

In the block's instance script, take them as a prop and hand them to the core. onStories and onStory are where the buttons go:

<!-- src/lib/components/ui/testimonials-cards.svelte, instance script -->
<script lang="ts">
  import { SvAvatar, SvRating, SvButton, SvToaster } from '@svgrid/grid'
  import { goto } from '$app/navigation'

  let { items }: { items?: Testimonial[] } = $props()
  const cards = createTestimonialCards({ items, onStories: () => goto('/customers') })
</script>
<!-- src/routes/+page.svelte -->
<script lang="ts">
  import TestimonialsCards from '$lib/components/ui/testimonials-cards.svelte'

  let { data } = $props()
</script>

<TestimonialsCards items={data.quotes} />

The carousel takes the same shape (CarouselQuote) plus interval in milliseconds (0 turns autoplay off) and onChange(index, item), which fires on every slide change, for analytics.

Headless

Each block here is a styled render of the core in its <script module>. To draw your own markup, take the core alone with npx @svgrid/ui add testimonials-cards --headless (it writes testimonials-cards.svelte.ts) and spread its prop-getters on plain elements:

<script lang="ts">
  import { createTestimonialCards } from './testimonials-cards.svelte.js'

  let { data } = $props()
  const cards = createTestimonialCards({ items: data.quotes, excerptLength: 200 })
</script>

{#each cards.items as t (t.id)}
  <figure>
    <blockquote {...cards.quoteProps(t)}>{cards.text(t)}</blockquote>
    {#if cards.isLong(t)}
      <button {...cards.toggleProps(t)}>{cards.isExpanded(t.id) ? 'Show less' : 'Read more'}</button>
    {/if}
    <figcaption>{t.name}, {t.role} at {t.company}</figcaption>
  </figure>
{/each}

toggleProps carries aria-expanded and an aria-controls that points at the quote's id from quoteProps. The carousel core has no carousel markup of its own: it gives you index (settable), next(), prev(), go(i), autoplay (the interval, or 0 while reducedMotion is true) and jumpProps(i) for buttons that jump to a quote, so you can drive SvCarousel or your own track with it.

See also

Live examples

  • Testimonial cards block - Six customer quote cards in a masonry-style column layout, each with a company wordmark, optional SvRating, the quote and an SvAvatar with name and role. Long quotes open with Read more, and the header shows the average rating.
  • Testimonial carousel block - One large customer quote at a time in an SvCarousel with arrows, dots, swipe and autoplay that pauses on hover or focus and stays off under reduced motion. A row of company wordmarks jumps to that company's quote.