hero.js: skriv aldri attributter på elementer appen hydrerer
lint-and-reload / lint (push) Successful in 2s

Headerens høyde og teksten sin uttoning lå som inline-stiler på
<header> og .hero-copy, satt før appen hydrerte; det kastet
hydreringen av sporet (uhhm sin hero viser det samme), så ruteren
aldri kom opp og hvert klikk ble en full sideinnlasting. Nå er de
custom properties på <body>, som appen ikke hydrerer, og CSS-en leser
dem.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
blandClaude Fable 5.1 committed 2026-10-21 15:08:20 +02:00
1 parent f61e3d2d70
commit 3522130b27
1 file changed
+22 -14
+22 -14
View File
@@ -149,8 +149,13 @@ const CSS = `
/* Sticky at the viewport top for the whole scroll (its containing
block is the page itself), z-index 0 so the cards (z-index 1)
stack over it. The height is a floor, frozen to a measured pixel
value by the module once it runs (see size()); the svh ladder is
the first paint and the no-JS fallback. A floor and not a height,
value by the module once it runs (see size()) - as a custom
property on <body>, never as an inline style on the header: the
module runs before the app hydrates, and an attribute written
onto an element the app is about to hydrate throws the hydration
off, and with it client-side navigation (uhhm's hero shows the
same). The svh ladder is the first paint and the no-JS fallback.
A floor and not a height,
so a copy that needs more than a screen (a phone) grows the header
rather than being clipped, and the cards start under it. */
position: sticky;
@@ -160,6 +165,7 @@ const CSS = `
width: 100%;
min-height: 100vh;
min-height: 100svh;
min-height: var(--westra-hero-h, 100svh);
padding: 0;
justify-content: flex-end;
overflow: hidden;
@@ -200,8 +206,11 @@ const CSS = `
transform: none;
}
/* The copy's fade is a custom property too, for the same reason. */
.hero-module .hero-copy {
position: static;
opacity: var(--westra-copy, 1);
visibility: var(--westra-copy-vis, visible);
padding: 0 1.5rem 3.5rem;
gap: 0.6rem;
text-shadow: 0 0.12em 1.4em var(--hero-glow);
@@ -325,7 +334,9 @@ class WestraHero {
ensureStyle();
this.stopped = false;
this.hero = container.closest('.hero') || container.parentElement;
this.copy = this.hero ? this.hero.querySelector('.hero-copy') : null;
// Where the module's custom properties go: an element the app
// never hydrates, so writing to it cannot disturb hydration.
this.host = document.body;
this.reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
this.dark = window.matchMedia('(prefers-color-scheme: dark)');
@@ -418,12 +429,11 @@ class WestraHero {
const p = ease(this.progress);
this.name.style.setProperty('--p', p.toFixed(4));
// The copy goes sooner than the name: it has to be gone before
// the first card reaches it, on a short screen too.
if (this.copy) {
const visible = 1 - clamp01(this.progress * 2.5);
this.copy.style.opacity = visible.toFixed(3);
this.copy.style.visibility = visible <= 0.01 ? 'hidden' : '';
}
// the first card reaches it, on a short screen too. Written
// on <body>, which the app does not hydrate (see the CSS).
const visible = 1 - clamp01(this.progress * 2.5);
this.host.style.setProperty('--westra-copy', visible.toFixed(3));
this.host.style.setProperty('--westra-copy-vis', visible <= 0.01 ? 'hidden' : 'visible');
this.draw(this.reduced.matches ? 0 : (now - this.t0) / 1000);
@@ -446,7 +456,7 @@ class WestraHero {
size() {
this.width = window.innerWidth;
this.height = Math.max(1, window.innerHeight);
if (this.hero) this.hero.style.minHeight = this.height + 'px';
this.host.style.setProperty('--westra-hero-h', this.height + 'px');
this.fit();
}
@@ -583,10 +593,8 @@ class WestraHero {
const lose = this.gl.getExtension('WEBGL_lose_context');
if (lose) lose.loseContext();
}
if (this.hero) this.hero.style.minHeight = '';
if (this.copy) {
this.copy.style.opacity = '';
this.copy.style.visibility = '';
for (const prop of ['--westra-hero-h', '--westra-copy', '--westra-copy-vis']) {
this.host.style.removeProperty(prop);
}
this.canvas.remove();
this.name.remove();