This commit is contained in:
Felix Klement committed 2026-09-18 19:25:48 +02:00
1 parent 0c2b4e7df2
commit e652b54319
11 files changed
+202 -12

No files matched your search

+55 -1
View File
@@ -9,6 +9,8 @@ const ui = t(lang);
---
<section class="hero night" aria-labelledby="hero-title">
<div class="hero__media">
<!-- The still is the video's first frame, so it carries the hero until the video is ready
and stays put for anyone who never gets one: no script, reduced motion, saved data. -->
<Picture
src={plant}
alt={ui.hero.imageAlt}
@@ -22,6 +24,18 @@ const ui = t(lang);
class="hero__img img-cover img-doc"
pictureAttributes={{ class: 'hero__picture' }}
/>
<video
class="hero__video img-cover img-doc"
muted
loop
playsinline
disablepictureinpicture
preload="none"
aria-hidden="true"
data-hero-video
data-mp4="/video/hero-plant.mp4"
data-webm="/video/hero-plant.webm"
></video>
<div class="hero__overlay"></div>
</div>
@@ -39,7 +53,8 @@ const ui = t(lang);
position: relative;
display: flex;
align-items: flex-end;
min-height: clamp(30rem, calc(100svh - var(--header-height)), 56rem);
/* Fills the screen under the header, and never less than the headline needs. */
min-height: max(28rem, calc(100svh - var(--header-height)));
overflow: hidden;
}
.hero__media { position: absolute; inset: 0; }
@@ -53,6 +68,18 @@ const ui = t(lang);
transform-origin: 50% 60%;
animation: hero-settle var(--dur-hero) var(--ease) both;
}
.hero__video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: 60% 50%;
opacity: 0;
transition: opacity var(--dur-slow) var(--ease);
}
/* Set once the video can actually play, so the still never cuts to black. */
.hero__video[data-ready] { opacity: 1; }
.hero__overlay {
position: absolute;
inset: 0;
@@ -82,5 +109,32 @@ const ui = t(lang);
@media (prefers-reduced-motion: reduce) {
.hero__img, :global(.hero__img), .hero__line { animation: none; opacity: 1; transform: none; }
.hero__video { display: none; }
}
</style>
<script>
/* The sources are attached here rather than in the markup, so the file is only fetched for
someone who will actually see it move: not under reduced motion, not on saved data, and not
on a phone, where a 21:9 frame is cropped to a sliver and the bytes cost the most. */
const video = document.querySelector<HTMLVideoElement>('[data-hero-video]');
const wants = window.matchMedia('(min-width: 48rem)').matches
&& !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const connection = (navigator as Navigator & { connection?: { saveData?: boolean } }).connection;
if (video && wants && !connection?.saveData) {
const add = (src: string | null, type: string) => {
if (!src) return;
const source = document.createElement('source');
source.src = src;
source.type = type;
video.append(source);
};
add(video.dataset.webm ?? null, 'video/webm');
add(video.dataset.mp4 ?? null, 'video/mp4');
video.addEventListener('canplay', () => video.setAttribute('data-ready', ''), { once: true });
video.preload = 'auto';
video.load();
/* A blocked autoplay leaves the still in place, which is a fine hero on its own. */
video.play().catch(() => {});
}
</script>