Zoeken...  ⌘K GitHub

HeroParallax Hero

Full-viewport hero met twee lagen parallax scrolling. Achtergrond beweegt langzaam, voorgrond sneller. Diepte-effect zonder externe libs.

/hero-parallax
src/components/hero/HeroParallax.astro
---
interface Props {
  headline: string;
  sub?: string;
  ctaLabel?: string;
  ctaHref?: string;
  secondaryLabel?: string;
  secondaryHref?: string;
  image1: string;
  image2?: string;
  overlay?: boolean;
}

const {
  headline,
  sub,
  ctaLabel,
  ctaHref = '#',
  secondaryLabel,
  secondaryHref = '#',
  image1,
  image2,
  overlay = true,
} = Astro.props;
---

<section class="hpar__root" aria-label="Hero">
  <!-- Background parallax layer -->
  <div class="hpar__bg" id="hpar-bg">
    <img
      src={image1}
      alt=""
      class="hpar__bg-img"
      loading="eager"
      fetchpriority="high"
      aria-hidden="true"
    />
    {overlay && <div class="hpar__overlay" aria-hidden="true"></div>}
  </div>

  <!-- Foreground parallax card -->
  {image2 && (
    <div class="hpar__fg" id="hpar-fg" aria-hidden="true">
      <img
        src={image2}
        alt=""
        class="hpar__fg-img"
        loading="eager"
        fetchpriority="high"
      />
    </div>
  )}

  <!-- Content -->
  <div class="hpar__content" id="hpar-content">
    <div class="hpar__inner">
      <h1 class="hpar__headline" set:html={headline} />
      {sub && <p class="hpar__sub">{sub}</p>}
      {(ctaLabel || secondaryLabel) && (
        <div class="hpar__actions">
          {ctaLabel && (
            <a href={ctaHref} class="hpar__cta hpar__cta--primary">
              {ctaLabel}
            </a>
          )}
          {secondaryLabel && (
            <a href={secondaryHref} class="hpar__cta hpar__cta--ghost">
              {secondaryLabel}
            </a>
          )}
        </div>
      )}
    </div>
  </div>
</section>

<script>
  (function () {
    const bg = document.getElementById('hpar-bg');
    const fg = document.getElementById('hpar-fg');
    const content = document.getElementById('hpar-content');

    if (!bg) return;

    let ticking = false;
    let lastY = 0;

    function applyParallax(scrollY: number) {
      // Background moves at 0.3x scroll speed (slow, pulls away)
      const bgOffset = scrollY * 0.3;
      bg!.style.transform = `translateY(${bgOffset}px)`;

      // Foreground moves at 0.6x scroll speed (faster = more depth)
      if (fg) {
        const fgOffset = scrollY * -0.6;
        fg.style.transform = `translateY(${fgOffset}px)`;
      }
    }

    function onScroll() {
      lastY = window.scrollY;
      if (!ticking) {
        requestAnimationFrame(() => {
          applyParallax(lastY);
          ticking = false;
        });
        ticking = true;
      }
    }

    // Entrance animation: observe content
    if (content) {
      const observer = new IntersectionObserver(
        (entries) => {
          entries.forEach((entry) => {
            if (entry.isIntersecting) {
              entry.target.classList.add('hpar__content--visible');
              observer.unobserve(entry.target);
            }
          });
        },
        { threshold: 0.1 }
      );
      observer.observe(content);
    }

    window.addEventListener('scroll', onScroll, { passive: true });
    // Run once on load
    applyParallax(window.scrollY);
  })();
</script>

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

  .hpar__root {
    position: relative;
    width: 100%;
    min-height: 100svh;
    overflow: hidden;
    display: flex;
    align-items: center;
    background: var(--color-primary);
  }

  /* Background layer */
  .hpar__bg {
    position: absolute;
    inset: -15% 0;
    will-change: transform;
    z-index: 0;
  }

  .hpar__bg-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
  }

  .hpar__overlay {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.45);
  }

  /* Foreground parallax card */
  .hpar__fg {
    position: absolute;
    right: 6%;
    top: 50%;
    transform: translateY(-50%);
    width: clamp(260px, 32vw, 520px);
    aspect-ratio: 4 / 5;
    border-radius: calc(var(--radius) * 2);
    overflow: hidden;
    box-shadow:
      0 32px 80px rgba(0, 0, 0, 0.5),
      0 8px 24px rgba(0, 0, 0, 0.35);
    will-change: transform;
    z-index: 2;
  }

  .hpar__fg-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
  }

  /* Content */
  .hpar__content {
    position: relative;
    z-index: 3;
    width: 100%;
    padding: clamp(2rem, 6vw, 6rem) clamp(1.5rem, 8vw, 8rem);
    opacity: 0;
    transform: translateY(24px);
    transition:
      opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1),
      transform 0.8s cubic-bezier(0.16, 1, 0.3, 1);
  }

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

  .hpar__inner {
    max-width: min(640px, 55vw);
  }

  .hpar__headline {
    font-size: clamp(3rem, 6vw, 5.5rem);
    font-weight: 900;
    line-height: 1.04;
    letter-spacing: -0.03em;
    color: #fff;
    margin: 0 0 1.25rem;
  }

  .hpar__headline em {
    font-style: normal;
    color: var(--color-accent);
  }

  .hpar__sub {
    font-size: clamp(1rem, 1.5vw, 1.25rem);
    color: rgba(255, 255, 255, 0.72);
    line-height: 1.65;
    margin: 0 0 2.5rem;
    max-width: 48ch;
  }

  .hpar__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.875rem;
    align-items: center;
  }

  .hpar__cta {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.8125rem 2rem;
    border-radius: var(--radius);
    font-size: 0.9375rem;
    font-weight: 600;
    text-decoration: none;
    transition:
      opacity 0.2s,
      transform 0.2s;
    white-space: nowrap;
  }

  .hpar__cta:hover {
    opacity: 0.88;
    transform: translateY(-1px);
  }

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

  .hpar__cta--ghost {
    background: transparent;
    color: #fff;
    border: 1.5px solid rgba(255, 255, 255, 0.4);
  }

  .hpar__cta--ghost:hover {
    border-color: rgba(255, 255, 255, 0.7);
  }

  /* Entrance animation for content (CSS fallback for no-JS) */
  @keyframes hpar-fadein {
    from {
      opacity: 0;
      transform: translateY(24px);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }

  /* Mobile */
  @media (max-width: 768px) {
    .hpar__inner {
      max-width: 100%;
    }

    .hpar__fg {
      display: none;
    }

    .hpar__content {
      padding: clamp(2rem, 8vw, 3rem) clamp(1.25rem, 5vw, 2rem);
    }
  }

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

Props

Prop Type Default Beschrijving
headline * string H1 — gebruik <em> voor accent
image1 * string Achtergrond afbeelding (beweegt langzaam)
image2 string Voorgrond kaart afbeelding (beweegt sneller)
overlay boolean true Donkere overlay over achtergrond
sub string Ondertitel
ctaLabel string Primaire CTA

* = verplicht