Zoeken...  ⌘K GitHub

FeaturesBento Sections

Bento-grid layout met variabele cel-groottes — tekst, stats, quotes en afbeeldingen.

/features-bento
src/components/sections/FeaturesBento.astro
---
/**
 * FeaturesBento
 * Bento-grid layout — visueel rijke feature showcase met variabele cel-groottes.
 */
interface BentoItem {
  size: 'sm' | 'md' | 'lg' | 'wide' | 'tall';
  type: 'text' | 'image' | 'stat' | 'quote';
  headline?: string;
  body?: string;
  image?: string;
  imageAlt?: string;
  stat?: string;
  statLabel?: string;
  quote?: string;
  quoteAuthor?: string;
  accent?: boolean;
  dark?: boolean;
}

interface Props {
  eyebrow?: string;
  headline?: string;
  items: BentoItem[];
}

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

<section class="fbt" data-component="features-bento">
  <div class="fbt__inner">
    {(eyebrow || headline) && (
      <div class="fbt__header">
        {eyebrow && <p class="fbt__eyebrow">{eyebrow}</p>}
        {headline && <h2 class="fbt__title" set:html={headline} />}
      </div>
    )}

    <div class="fbt__grid">
      {items.map(item => (
        <div class:list={[
          'fbt__cell',
          `fbt__cell--${item.size}`,
          { 'fbt__cell--accent': item.accent },
          { 'fbt__cell--dark': item.dark },
          { 'fbt__cell--image': item.type === 'image' },
        ]}>
          {item.type === 'image' && item.image && (
            <img src={item.image} alt={item.imageAlt ?? ''} class="fbt__cell-img" />
          )}
          {item.type === 'stat' && (
            <div class="fbt__stat">
              <div class="fbt__stat-value">{item.stat}</div>
              <div class="fbt__stat-label">{item.statLabel}</div>
            </div>
          )}
          {item.type === 'quote' && (
            <div class="fbt__quote">
              <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" opacity="0.3">
                <path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z"/>
              </svg>
              <p class="fbt__quote-text">{item.quote}</p>
              {item.quoteAuthor && <p class="fbt__quote-author">— {item.quoteAuthor}</p>}
            </div>
          )}
          {item.type === 'text' && (
            <div class="fbt__text">
              {item.headline && <h3 class="fbt__cell-headline">{item.headline}</h3>}
              {item.body && <p class="fbt__cell-body">{item.body}</p>}
            </div>
          )}
        </div>
      ))}
    </div>
  </div>
</section>

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

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

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

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

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

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

  /* Grid */
  .fbt__grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    grid-auto-rows: 200px;
    gap: 1rem;
  }

  .fbt__cell {
    background: #fff;
    border-radius: calc(var(--radius, 0.5rem) * 2);
    padding: 1.75rem;
    overflow: hidden;
    position: relative;
    display: flex;
    align-items: flex-end;
  }

  .fbt__cell--sm { grid-column: span 1; }
  .fbt__cell--md { grid-column: span 2; }
  .fbt__cell--lg { grid-column: span 2; grid-row: span 2; }
  .fbt__cell--wide { grid-column: span 3; }
  .fbt__cell--tall { grid-column: span 1; grid-row: span 2; }

  .fbt__cell--accent {
    background: var(--color-accent, #6366f1);
    color: #fff;
  }

  .fbt__cell--dark {
    background: var(--color-primary, #0a0a0a);
    color: #fff;
  }

  .fbt__cell--image {
    padding: 0;
    align-items: stretch;
  }

  .fbt__cell-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: inherit;
  }

  /* Stat */
  .fbt__stat {
    display: flex;
    flex-direction: column;
    width: 100%;
  }

  .fbt__stat-value {
    font-size: 3.5rem;
    font-weight: 800;
    letter-spacing: -0.04em;
    line-height: 1;
    margin-bottom: 0.375rem;
  }

  .fbt__cell--accent .fbt__stat-value { color: #fff; }
  .fbt__cell--dark .fbt__stat-value { color: #fff; }

  .fbt__stat-label {
    font-size: 0.875rem;
    opacity: 0.6;
  }

  /* Quote */
  .fbt__quote {
    display: flex;
    flex-direction: column;
    width: 100%;
    gap: 0.75rem;
  }

  .fbt__quote-text {
    font-size: 1rem;
    line-height: 1.6;
    font-weight: 500;
  }

  .fbt__quote-author {
    font-size: 0.8125rem;
    opacity: 0.55;
  }

  /* Text */
  .fbt__text { width: 100%; }

  .fbt__cell-headline {
    font-size: 1.125rem;
    font-weight: 700;
    margin-bottom: 0.5rem;
    color: var(--color-primary, #0a0a0a);
  }

  .fbt__cell--accent .fbt__cell-headline,
  .fbt__cell--dark .fbt__cell-headline { color: #fff; }

  .fbt__cell-body {
    font-size: 0.9rem;
    line-height: 1.6;
    color: var(--color-muted, #6b7280);
  }

  .fbt__cell--accent .fbt__cell-body,
  .fbt__cell--dark .fbt__cell-body { color: rgba(255,255,255,0.65); }

  @media (max-width: 900px) {
    .fbt__grid { grid-template-columns: repeat(2, 1fr); }
    .fbt__cell--wide { grid-column: span 2; }
    .fbt__cell--lg { grid-column: span 2; }
  }

  @media (max-width: 540px) {
    .fbt__grid { grid-template-columns: 1fr; grid-auto-rows: auto; }
    .fbt__cell { min-height: 160px; }
    .fbt__cell--sm,
    .fbt__cell--md,
    .fbt__cell--lg,
    .fbt__cell--wide,
    .fbt__cell--tall { grid-column: span 1; grid-row: span 1; }
  }
</style>

Props

Prop Type Default Beschrijving
items * BentoItem[] Grid items. Grootte: sm/md/lg/wide/tall. Type: text/image/stat/quote.
eyebrow string Label boven sectie
headline string Sectie headline

* = verplicht