Zoeken...  ⌘K GitHub

ContentColumns content

Krant-stijl meerdere kolommen. Drop cap optioneel. Verticale scheidingslijnen. 2 of 3 kolommen.

/content-columns
src/components/content/ContentColumns.astro
---
interface Column {
  title?: string;
  body: string;
  icon?: string;
}

interface Props {
  eyebrow?: string;
  headline?: string;
  columns: Column[];
  columnCount?: 2 | 3;
  dropCap?: boolean;
}

const {
  eyebrow,
  headline,
  columns = [],
  columnCount = 3,
  dropCap = false,
} = Astro.props;

const count = Math.min(columnCount, columns.length) as 2 | 3;
---

<section class={`ccl__section ccl__section--cols-${count}`}>
  {(eyebrow || headline) && (
    <div class="ccl__header ccl-reveal">
      {eyebrow && <p class="ccl__eyebrow">{eyebrow}</p>}
      {headline && <h2 class="ccl__headline">{headline}</h2>}
    </div>
  )}

  <div class={`ccl__grid ccl__grid--${count}`}>
    {columns.map((col, i) => (
      <div
        class={`ccl__col ccl-reveal${dropCap && i === 0 ? ' ccl__col--dropcap' : ''}${i < columns.length - 1 ? ' ccl__col--divided' : ''}`}
        style={`animation-delay: ${i * 0.1}s; transition-delay: ${i * 0.1}s;`}
      >
        {col.icon && (
          <div class="ccl__icon" aria-hidden="true" set:html={col.icon} />
        )}
        {col.title && <h3 class="ccl__col-title">{col.title}</h3>}
        <p class="ccl__col-body">{col.body}</p>
      </div>
    ))}
  </div>
</section>

<script>
  const reveals = document.querySelectorAll<HTMLElement>('.ccl-reveal');

  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          (entry.target as HTMLElement).classList.add('ccl-reveal--visible');
          observer.unobserve(entry.target);
        }
      });
    },
    { threshold: 0.2 }
  );

  reveals.forEach((el) => observer.observe(el));
</script>

<style>
  :root {
    --color-primary: #0a0a0a;
    --color-accent: #6366f1;
    --color-bg: #fff;
    --color-muted: #6b7280;
    --radius: 0.5rem;
  }

  .ccl__section {
    background: var(--color-bg);
    padding: clamp(3rem, 8vw, 7rem) clamp(1rem, 5vw, 2rem);
  }

  /* Header */
  .ccl__header {
    max-width: 760px;
    margin: 0 auto clamp(2.5rem, 5vw, 4rem);
    text-align: center;
  }

  .ccl__eyebrow {
    font-size: clamp(0.65rem, 1.2vw, 0.75rem);
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--color-accent);
    margin: 0 0 0.75rem;
  }

  .ccl__headline {
    font-size: clamp(1.75rem, 3vw, 2.75rem);
    font-weight: 800;
    line-height: 1.2;
    color: var(--color-primary);
    margin: 0;
  }

  /* Grid */
  .ccl__grid {
    display: grid;
    max-width: 1200px;
    margin: 0 auto;
    align-items: start;
  }

  .ccl__grid--3 {
    grid-template-columns: repeat(3, 1fr);
    gap: 0;
  }

  .ccl__grid--2 {
    grid-template-columns: repeat(2, 1fr);
    gap: 0;
  }

  /* Columns */
  .ccl__col {
    padding: 0 clamp(1.25rem, 3vw, 2.5rem);
  }

  .ccl__col--divided {
    border-right: 1px solid rgba(0, 0, 0, 0.08);
  }

  /* Drop cap */
  .ccl__col--dropcap .ccl__col-body::first-letter {
    float: left;
    font-size: clamp(3.5rem, 6vw, 5rem);
    font-weight: 900;
    line-height: 0.8;
    margin: 0.05em 0.1em 0 0;
    color: var(--color-accent);
    font-family: Georgia, 'Times New Roman', serif;
  }

  /* Icon */
  .ccl__icon {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    margin-bottom: 1rem;
    color: var(--color-accent);
  }

  .ccl__icon :global(svg) {
    width: 2rem;
    height: 2rem;
    flex-shrink: 0;
  }

  /* Column content */
  .ccl__col-title {
    font-size: clamp(1rem, 1.5vw, 1.125rem);
    font-weight: 700;
    color: var(--color-primary);
    margin: 0 0 0.75rem;
    line-height: 1.3;
  }

  .ccl__col-body {
    font-size: clamp(0.9375rem, 1.3vw, 1rem);
    line-height: 1.75;
    color: var(--color-muted);
    margin: 0;
  }

  /* Scroll reveal */
  .ccl-reveal {
    opacity: 0;
    transform: translateY(1.5rem);
    transition: opacity 0.6s ease, transform 0.6s ease;
  }

  .ccl-reveal--visible {
    opacity: 1;
    transform: translateY(0);
  }

  /* Responsive */
  @media (max-width: 768px) {
    .ccl__grid--3 {
      grid-template-columns: repeat(2, 1fr);
    }

    .ccl__col {
      padding: clamp(1rem, 3vw, 1.5rem);
      border-right: none !important;
    }

    /* Restore dividers in 2-col layout: right column of each row */
    .ccl__grid--3 .ccl__col:nth-child(odd):not(:last-child) {
      border-right: 1px solid rgba(0, 0, 0, 0.08) !important;
    }

    .ccl__grid--3 .ccl__col,
    .ccl__grid--2 .ccl__col {
      border-bottom: 1px solid rgba(0, 0, 0, 0.06);
    }

    .ccl__grid--3 .ccl__col:last-child,
    .ccl__grid--2 .ccl__col:last-child {
      border-bottom: none;
    }
  }

  @media (max-width: 480px) {
    .ccl__grid--3,
    .ccl__grid--2 {
      grid-template-columns: 1fr;
    }

    .ccl__grid--3 .ccl__col:nth-child(odd):not(:last-child) {
      border-right: none !important;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    * {
      animation: none !important;
      transition: none !important;
    }
  }
</style>

Props

Prop Type Default Beschrijving
columns * { title?: string; body: string; icon?: string }[] Kolom inhoud
columnCount 2 | 3 3 Aantal kolommen
dropCap boolean false Grote eerste letter in eerste kolom
dividers boolean false Verticale scheidingslijnen

* = verplicht