Zoeken...  ⌘K GitHub

ContentSplit content

Tekst links + afbeelding rechts (of omgekeerd). IntersectionObserver slide-in. Ideaal voor dienst- of feature uitleg.

/content-split
src/components/content/ContentSplit.astro
---
interface Props {
  eyebrow?: string;
  headline: string;
  body: string;
  ctaLabel?: string;
  ctaHref?: string;
  secondaryLabel?: string;
  secondaryHref?: string;
  image: string;
  imageAlt?: string;
  reversed?: boolean;
  imageFit?: 'cover' | 'contain';
}

const {
  eyebrow,
  headline,
  body,
  ctaLabel,
  ctaHref = '#',
  secondaryLabel,
  secondaryHref = '#',
  image,
  imageAlt = '',
  reversed = false,
  imageFit = 'cover',
} = Astro.props;
---

<section class={`csp__section${reversed ? ' csp__section--reversed' : ''}`}>
  <div class="csp__inner">
    <div class="csp__text csp-reveal csp-reveal--left">
      {eyebrow && <p class="csp__eyebrow">{eyebrow}</p>}
      <h2 class="csp__headline" set:html={headline} />
      <p class="csp__body">{body}</p>
      {(ctaLabel || secondaryLabel) && (
        <div class="csp__actions">
          {ctaLabel && (
            <a href={ctaHref} class="csp__cta csp__cta--primary">{ctaLabel}</a>
          )}
          {secondaryLabel && (
            <a href={secondaryHref} class="csp__cta csp__cta--secondary">{secondaryLabel}</a>
          )}
        </div>
      )}
    </div>

    <div class="csp__media csp-reveal csp-reveal--right">
      <img
        src={image}
        alt={imageAlt}
        class="csp__image"
        style={`object-fit: ${imageFit};`}
        loading="lazy"
        decoding="async"
      />
    </div>
  </div>
</section>

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

  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          (entry.target as HTMLElement).classList.add('csp-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;
  }

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

  .csp__inner {
    display: flex;
    align-items: center;
    gap: clamp(2rem, 5vw, 5rem);
    max-width: 1200px;
    margin: 0 auto;
  }

  .csp__section--reversed .csp__inner {
    flex-direction: row-reverse;
  }

  /* Text side */
  .csp__text {
    flex: 1 1 45%;
    min-width: 0;
  }

  .csp__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;
  }

  .csp__headline {
    font-size: clamp(2rem, 3.5vw, 3rem);
    font-weight: 800;
    line-height: 1.15;
    color: var(--color-primary);
    margin: 0 0 1.25rem;
  }

  .csp__headline :global(em) {
    font-style: italic;
    color: var(--color-accent);
  }

  .csp__body {
    font-size: clamp(1rem, 1.5vw, 1.125rem);
    line-height: 1.7;
    color: var(--color-muted);
    margin: 0 0 2rem;
  }

  .csp__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
  }

  .csp__cta {
    display: inline-flex;
    align-items: center;
    padding: 0.75rem 1.5rem;
    border-radius: var(--radius);
    font-size: 0.9375rem;
    font-weight: 600;
    text-decoration: none;
    transition: opacity 0.2s ease, transform 0.2s ease;
  }

  .csp__cta:hover {
    opacity: 0.85;
    transform: translateY(-1px);
  }

  .csp__cta--primary {
    background: var(--color-accent);
    color: #fff;
  }

  .csp__cta--secondary {
    background: transparent;
    color: var(--color-primary);
    border: 1.5px solid rgba(0, 0, 0, 0.15);
  }

  /* Image side */
  .csp__media {
    flex: 1 1 55%;
    min-width: 0;
  }

  .csp__image {
    display: block;
    width: 100%;
    height: clamp(280px, 45vw, 560px);
    object-position: center;
    border-radius: var(--radius);
    box-shadow: 0 8px 40px rgba(0, 0, 0, 0.1), 0 2px 8px rgba(0, 0, 0, 0.06);
  }

  /* Scroll reveal */
  .csp-reveal {
    opacity: 0;
    transition: opacity 0.7s ease, transform 0.7s ease;
  }

  .csp-reveal--left {
    transform: translateX(-2.5rem);
  }

  .csp-reveal--right {
    transform: translateX(2.5rem);
    transition-delay: 0.1s;
  }

  .csp__section--reversed .csp-reveal--left {
    transform: translateX(-2.5rem);
  }

  .csp__section--reversed .csp-reveal--right {
    transform: translateX(2.5rem);
  }

  .csp-reveal--visible {
    opacity: 1;
    transform: translateX(0);
  }

  /* Responsive */
  @media (max-width: 768px) {
    .csp__inner,
    .csp__section--reversed .csp__inner {
      flex-direction: column;
    }

    .csp__text,
    .csp__media {
      flex: 1 1 100%;
      width: 100%;
    }

    .csp__media {
      order: -1;
    }

    .csp-reveal--left,
    .csp-reveal--right {
      transform: translateY(1.5rem);
    }

    .csp-reveal--visible {
      transform: translateY(0);
    }
  }

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

Props

Prop Type Default Beschrijving
headline * string Sectie headline — gebruik <em> voor accent
body * string Tekst blok (paragraph)
image * string Afbeelding URL
reversed boolean false Afbeelding links, tekst rechts
eyebrow string Klein label boven headline
ctaLabel string CTA knop tekst

* = verplicht