Zoeken...  ⌘K GitHub

TestimonialsMasonry Social Proof

Masonry testimonial grid — variabele hoogtes, featured card in accent kleur.

/testimonials-masonry
src/components/social-proof/TestimonialsMasonry.astro
---
/**
 * TestimonialsMasonry
 * Masonry-stijl testimonial grid — variabele hoogtes, social-proof cards.
 */
interface Testimonial {
  text: string;
  author: string;
  role?: string;
  company?: string;
  avatar?: string;
  rating?: number;
  featured?: boolean;
}

interface Props {
  eyebrow?: string;
  headline?: string;
  testimonials: Testimonial[];
}

const { eyebrow, headline, testimonials = [] } = Astro.props;
---

<section class="tm" data-component="testimonials-masonry">
  <div class="tm__inner">
    {(eyebrow || headline) && (
      <div class="tm__header">
        {eyebrow && <p class="tm__eyebrow">{eyebrow}</p>}
        {headline && <h2 class="tm__title" set:html={headline} />}
      </div>
    )}

    <div class="tm__masonry">
      {testimonials.map(t => (
        <div class:list={['tm__card', { 'tm__card--featured': t.featured }]}>
          {t.rating && (
            <div class="tm__stars">
              {Array.from({ length: t.rating }).map(() => (
                <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
                  <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
                </svg>
              ))}
            </div>
          )}
          <p class="tm__text">{t.text}</p>
          <div class="tm__author">
            {t.avatar && <img src={t.avatar} alt={t.author} class="tm__avatar" />}
            <div>
              <div class="tm__author-name">{t.author}</div>
              {(t.role || t.company) && (
                <div class="tm__author-meta">
                  {t.role}{t.role && t.company ? ', ' : ''}{t.company}
                </div>
              )}
            </div>
          </div>
        </div>
      ))}
    </div>
  </div>
</section>

<style>
  .tm {
    background: #f5f5f7;
    padding: 5rem 1.5rem;
  }

  .tm__inner {
    max-width: 1200px;
    margin: 0 auto;
  }

  .tm__header {
    text-align: center;
    margin-bottom: 3rem;
  }

  .tm__eyebrow {
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--color-accent, #6366f1);
    margin-bottom: 0.75rem;
  }

  .tm__title {
    font-size: clamp(1.875rem, 3vw, 2.75rem);
    font-weight: 800;
    letter-spacing: -0.03em;
    color: var(--color-primary, #0a0a0a);
  }

  .tm__title :global(em) {
    font-style: normal;
    color: var(--color-accent, #6366f1);
  }

  /* Masonry */
  .tm__masonry {
    columns: 3;
    column-gap: 1.25rem;
  }

  .tm__card {
    break-inside: avoid;
    background: #fff;
    border-radius: calc(var(--radius, 0.5rem) * 1.5);
    padding: 1.5rem;
    margin-bottom: 1.25rem;
    border: 1px solid rgba(0,0,0,0.06);
    transition: box-shadow 0.2s, transform 0.2s;
  }

  .tm__card:hover {
    box-shadow: 0 8px 32px rgba(0,0,0,0.08);
    transform: translateY(-2px);
  }

  .tm__card--featured {
    background: var(--color-accent, #6366f1);
    color: #fff;
    border-color: transparent;
  }

  .tm__stars {
    display: flex;
    gap: 2px;
    margin-bottom: 0.875rem;
    color: #f59e0b;
  }

  .tm__card--featured .tm__stars { color: rgba(255,255,255,0.9); }

  .tm__text {
    font-size: 0.9375rem;
    line-height: 1.65;
    color: var(--color-primary, #0a0a0a);
    margin-bottom: 1.25rem;
  }

  .tm__card--featured .tm__text { color: rgba(255,255,255,0.9); }

  .tm__author {
    display: flex;
    align-items: center;
    gap: 0.625rem;
  }

  .tm__avatar {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
  }

  .tm__author-name {
    font-size: 0.875rem;
    font-weight: 700;
    color: var(--color-primary, #0a0a0a);
  }

  .tm__card--featured .tm__author-name { color: #fff; }

  .tm__author-meta {
    font-size: 0.8125rem;
    color: var(--color-muted, #6b7280);
  }

  .tm__card--featured .tm__author-meta { color: rgba(255,255,255,0.65); }

  @media (max-width: 900px) {
    .tm__masonry { columns: 2; }
  }

  @media (max-width: 540px) {
    .tm__masonry { columns: 1; }
  }
</style>

Props

Prop Type Default Beschrijving
testimonials * Testimonial[] Array van testimonials. featured: true voor accent kaart.
eyebrow string Label boven sectie
headline string Sectie headline

* = verplicht