Skip to content

Single testimonial with outcomes

One quote given a whole section, paired with the numbers behind it.

Marketingstartertestimonialsoutcomemetricssingle

Live preview

full widthLive preview — open it in a new tab for the full-height version.
Open the preview in a new tab

Source

This exact file renders the preview above.

import { Container } from '@/components/ui/layout'
import { Avatar } from '@/components/ui/avatar'
import { Metric } from '@/components/ui/metric'
import { testimonials } from '@/content/demo'

/**
 * Single large testimonial with outcome figures
 *
 * One quote given the room of a whole section, paired with the numbers behind
 * it. A quote without an outcome is an opinion; the figures are what make it a
 * case for the product.
 */
export default function SingleLargeTestimonial() {
  const quote = testimonials[2]

  return (
    <section className="border-b border-line bg-surface-sunken py-section">
      <Container>
        <div className="grid gap-10 lg:grid-cols-[1.3fr_1fr] lg:gap-16">
          <figure>
            <blockquote className="display-type text-xl leading-snug font-medium text-ink-strong text-balance sm:text-2xl lg:text-3xl">
              “{quote?.quote}”
            </blockquote>
            <figcaption className="mt-8 flex items-center gap-3.5">
              <Avatar name={quote?.name ?? 'Anonymous'} size="lg" decorative />
              <div>
                <p className="text-sm font-semibold text-ink-strong">{quote?.name}</p>
                <p className="text-sm text-ink-muted">
                  {quote?.role}, {quote?.company}
                </p>
              </div>
            </figcaption>
          </figure>

          <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-1">
            <Metric label="Stale documentation examples" value="0" />
            <Metric label="Documentation contributions" value="×3" deltaLabel="per month" />
            <Metric label="Support tickets about the docs" value="−34%" delta={-34} invertTrend />
          </div>
        </div>
      </Container>
    </section>
  )
}

components/blocks/sections/testimonials/single-large.tsx

import { Container } from '@/components/ui/layout'
import { Avatar } from '@/components/ui/avatar'
import { Metric } from '@/components/ui/metric'
import { testimonials } from '@/content/demo'

/**
 * Single large testimonial with outcome figures
 *
 * One quote given the room of a whole section, paired with the numbers behind
 * it. A quote without an outcome is an opinion; the figures are what make it a
 * case for the product.
 */
export default function SingleLargeTestimonial() {
  const quote = testimonials[2]

  return (
    <section className="border-b border-line bg-surface-sunken py-section">
      <Container>
        <div className="grid gap-10 lg:grid-cols-[1.3fr_1fr] lg:gap-16">
          <figure>
            <blockquote className="display-type text-xl leading-snug font-medium text-ink-strong text-balance sm:text-2xl lg:text-3xl">
              “{quote?.quote}”
            </blockquote>
            <figcaption className="mt-8 flex items-center gap-3.5">
              <Avatar name={quote?.name ?? 'Anonymous'} size="lg" decorative />
              <div>
                <p className="text-sm font-semibold text-ink-strong">{quote?.name}</p>
                <p className="text-sm text-ink-muted">
                  {quote?.role}, {quote?.company}
                </p>
              </div>
            </figcaption>
          </figure>

          <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-1">
            <Metric label="Stale documentation examples" value="0" />
            <Metric label="Documentation contributions" value="×3" deltaLabel="per month" />
            <Metric label="Support tickets about the docs" value="−34%" delta={-34} invertTrend />
          </div>
        </div>
      </Container>
    </section>
  )
}

components/ui/metric.tsx

import type { ReactNode } from 'react'
import { ArrowUpRight, ArrowDownRight, Minus } from 'lucide-react'
import { cn } from '@/lib/cn'

/**
 * Metric
 *
 * A single number with optional trend. The delta carries an arrow *and* a sign
 * *and* a colour, so the direction survives a greyscale print or a colour
 * vision deficiency.
 *
 * `<dl>` semantics pair the label with the value; the sparkline is decorative
 * and hidden from assistive tech, since the number beside it is the content.
 */
export interface MetricProps {
  label: string
  value: string
  /** Percentage change. Positive is not automatically "good" — see `invertTrend`. */
  delta?: number
  deltaLabel?: string
  /** For metrics where down is good, e.g. churn or latency. */
  invertTrend?: boolean
  icon?: ReactNode
  /** 0–1 values rendered as a decorative sparkline. */
  sparkline?: number[]
  appearance?: 'plain' | 'card'
  className?: string
}

function Sparkline({ points }: { points: number[] }) {
  if (points.length < 2) return null
  const max = Math.max(...points)
  const min = Math.min(...points)
  const range = max - min || 1
  const path = points
    .map((point, index) => {
      const x = (index / (points.length - 1)) * 100
      const y = 28 - ((point - min) / range) * 26 - 1
      return `${index === 0 ? 'M' : 'L'}${x.toFixed(2)},${y.toFixed(2)}`
    })
    .join(' ')

  return (
    <svg viewBox="0 0 100 28" preserveAspectRatio="none" className="h-7 w-full" aria-hidden="true">
      <path
        d={path}
        fill="none"
        stroke="var(--color-accent)"
        strokeWidth="1.5"
        vectorEffect="non-scaling-stroke"
      />
    </svg>
  )
}

export function Metric({
  label,
  value,
  delta,
  deltaLabel,
  invertTrend = false,
  icon,
  sparkline,
  appearance = 'card',
  className,
}: MetricProps) {
  const direction = delta === undefined ? 'flat' : delta > 0 ? 'up' : delta < 0 ? 'down' : 'flat'
  const good = direction === 'flat' ? null : invertTrend ? direction === 'down' : direction === 'up'
  const TrendIcon =
    direction === 'up' ? ArrowUpRight : direction === 'down' ? ArrowDownRight : Minus

  return (
    <dl
      className={cn(
        'flex min-w-0 flex-col gap-1',
        appearance === 'card' && 'rounded-lg border border-line bg-surface p-card',
        className,
      )}
    >
      <div className="flex items-center justify-between gap-2">
        <dt className="label-caps truncate text-ink-subtle">{label}</dt>
        {icon ? <span className="shrink-0 text-ink-subtle">{icon}</span> : null}
      </div>
      <dd className="display-type text-2xl leading-none font-semibold text-ink-strong tabular-nums">
        {value}
      </dd>
      {delta !== undefined ? (
        <dd
          className={cn(
            'flex items-center gap-1 text-xs font-medium',
            good === null ? 'text-ink-muted' : good ? 'text-success' : 'text-danger',
          )}
        >
          <TrendIcon className="size-3.5 shrink-0" aria-hidden="true" />
          <span>
            {delta > 0 ? '+' : ''}
            {delta}%
          </span>
          {deltaLabel ? <span className="text-ink-subtle">{deltaLabel}</span> : null}
        </dd>
      ) : null}
      {sparkline ? (
        <dd className="mt-1">
          <Sparkline points={sparkline} />
        </dd>
      ) : null}
    </dl>
  )
}

Demo source — adapt to your project. Foundry is not published as a package.

Usage

A quote without an outcome is an opinion. The figures are what turn it into a case for the product.

  • Use your single strongest quote; this layout has nowhere to hide a weak one.
  • Outcome figures should come from the same customer as the quote.

Variants and states

Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.

  • Oversized quote
  • Three outcome metrics
  • Two-column at lg

Accessibility

Figure semantics
The quote and its attribution are one figure.
Metric pairing
Outcomes are description lists, not styled paragraphs.

Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.