Zoeken...  ⌘K GitHub

VideoHero video

Hero met grote thumbnail en play button. Klikken opent YouTube embed of direct video. Geen preload.

/video-hero
src/components/video/VideoHero.astro
---
interface Props {
  eyebrow?: string;
  headline: string;
  sub?: string;
  ctaLabel?: string;
  ctaHref?: string;
  thumbnail: string;
  videoUrl?: string;
  videoId?: string;
}

const {
  eyebrow,
  headline,
  sub,
  ctaLabel,
  ctaHref,
  thumbnail,
  videoUrl,
  videoId,
} = Astro.props;

// Build embed URL
let embedUrl = '';
if (videoId) {
  embedUrl = `https://www.youtube.com/embed/${videoId}?autoplay=1&rel=0`;
} else if (videoUrl) {
  // Check if YouTube or Vimeo URL
  const ytMatch = videoUrl.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\s]+)/);
  const vmMatch = videoUrl.match(/vimeo\.com\/(\d+)/);
  if (ytMatch) {
    embedUrl = `https://www.youtube.com/embed/${ytMatch[1]}?autoplay=1&rel=0`;
  } else if (vmMatch) {
    embedUrl = `https://player.vimeo.com/video/${vmMatch[1]}?autoplay=1`;
  }
}

const isDirectVideo = videoUrl && !embedUrl;
---

<section class="vh__section">
  {(eyebrow || headline || sub) && (
    <div class="vh__header">
      {eyebrow && <p class="vh__eyebrow">{eyebrow}</p>}
      {headline && <h1 class="vh__headline" set:html={headline} />}
      {sub && <p class="vh__sub">{sub}</p>}
    </div>
  )}

  <div class="vh__frame-wrap">
    <div class="vh__frame">
      <!-- Thumbnail layer -->
      <div class="vh__thumbnail" data-thumbnail>
        <img
          src={thumbnail}
          alt="Video thumbnail"
          class="vh__thumb-img"
          loading="lazy"
          decoding="async"
        />
        <button class="vh__play-btn" aria-label="Video afspelen">
          <svg width="28" height="28" viewBox="0 0 24 24" fill="currentColor">
            <polygon points="5,3 19,12 5,21" />
          </svg>
        </button>
      </div>

      <!-- Video layer (hidden until play) -->
      {embedUrl && (
        <div class="vh__video-layer" data-video-layer hidden>
          <iframe
            class="vh__iframe"
            data-src={embedUrl}
            src=""
            allow="autoplay; fullscreen; picture-in-picture"
            allowfullscreen
            title={headline}
            frameborder="0"
          />
        </div>
      )}

      {isDirectVideo && (
        <div class="vh__video-layer" data-video-layer hidden>
          <video
            class="vh__video"
            data-src={videoUrl}
            src=""
            autoplay
            controls
            playsinline
          />
        </div>
      )}
    </div>
  </div>

  {ctaLabel && ctaHref && (
    <div class="vh__cta-wrap">
      <a href={ctaHref} class="vh__cta">{ctaLabel}</a>
    </div>
  )}
</section>

<script>
  document.querySelectorAll<HTMLElement>('.vh__frame').forEach((frame) => {
    const playBtn = frame.querySelector<HTMLButtonElement>('.vh__play-btn');
    const thumbnail = frame.querySelector<HTMLElement>('[data-thumbnail]');
    const videoLayer = frame.querySelector<HTMLElement>('[data-video-layer]');
    const iframe = videoLayer?.querySelector<HTMLIFrameElement>('.vh__iframe');
    const video = videoLayer?.querySelector<HTMLVideoElement>('.vh__video');

    playBtn?.addEventListener('click', () => {
      if (!videoLayer) return;

      // Load the real src
      if (iframe) {
        iframe.src = iframe.dataset.src ?? '';
      }
      if (video) {
        video.src = video.dataset.src ?? '';
        video.play();
      }

      thumbnail?.setAttribute('hidden', '');
      videoLayer.removeAttribute('hidden');
    });
  });
</script>

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

  .vh__section {
    padding: 4rem 1.5rem;
    background: var(--color-bg);
  }

  .vh__header {
    text-align: center;
    max-width: 720px;
    margin: 0 auto 2.5rem;
  }

  .vh__eyebrow {
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--color-accent);
    margin: 0 0 0.75rem;
  }

  .vh__headline {
    font-size: clamp(2rem, 5vw, 3.25rem);
    font-weight: 700;
    color: var(--color-primary);
    margin: 0 0 1rem;
    line-height: 1.1;
  }

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

  .vh__sub {
    font-size: 1.0625rem;
    color: var(--color-muted);
    margin: 0;
    line-height: 1.6;
  }

  .vh__frame-wrap {
    max-width: 960px;
    margin: 0 auto;
    border-radius: var(--radius);
    overflow: hidden;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.12);
  }

  .vh__frame {
    position: relative;
    aspect-ratio: 16 / 9;
    background: #000;
    overflow: hidden;
    border-radius: var(--radius);
  }

  .vh__thumbnail {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .vh__thumb-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: filter 0.3s ease;
  }

  .vh__thumbnail:hover .vh__thumb-img {
    filter: brightness(0.85);
  }

  .vh__play-btn {
    position: absolute;
    width: 80px;
    height: 80px;
    border-radius: 50%;
    background: #fff;
    border: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--color-primary);
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
    transition: transform 0.2s ease, box-shadow 0.2s ease;
    padding-left: 4px; /* optical centering for triangle */
  }

  .vh__play-btn:hover {
    transform: scale(1.08);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
  }

  .vh__video-layer {
    position: absolute;
    inset: 0;
  }

  .vh__iframe,
  .vh__video {
    width: 100%;
    height: 100%;
    border: none;
    display: block;
  }

  .vh__cta-wrap {
    text-align: center;
    margin-top: 2rem;
  }

  .vh__cta {
    display: inline-block;
    padding: 0.75rem 2rem;
    background: var(--color-accent);
    color: #fff;
    text-decoration: none;
    border-radius: var(--radius);
    font-weight: 600;
    font-size: 0.9375rem;
    transition: opacity 0.2s ease, transform 0.2s ease;
  }

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

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

Props

Prop Type Default Beschrijving
thumbnail * string Poster/thumbnail afbeelding URL
videoId string YouTube video ID (kortste manier)
videoUrl string Directe video URL (YouTube/Vimeo/mp4)
headline string Optionele headline boven video
eyebrow string Label boven headline

* = verplicht