VideoHero video
Hero met grote thumbnail en play button. Klikken opent YouTube embed of direct video. Geen preload.
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