Hoppa till innehållet

18 oktober 2026 · 4 min läsning

En sida som zoomar

Hur startsidan på den här webbplatsen fungerar, varför den saknar meny, och vad jag lärde mig om övertoningar den hårda vägen.

De flesta personliga webbplatser är samma sida. En sidhuvudsrad med fyra länkar, en hero, ett rutnät av kort, en sidfot. Jag har byggt den sidan mer än en gång, och jag ville att den här skulle kännas som att lämna resten av internet en stund. Så startsidan här är ett enda föremål. Du kommer fram och ser ett namn. Att scrolla flyttar dig inte nedåt, det tar dig närmare: in i en mening, in i en fras i den meningen, in i projektet bakom frasen, och till sist in i en liten bit av dess kod. Sedan scrollar du upp igen och går.

Den här anteckningen handlar om hur det fungerar, för mekanismen är liten och jag tycker den är värd att dela.

Fem lager, en scroll

Dokumentet självt är fem tomma block, vart och ett en skärmhöjd högt. Det är allt webbläsaren scrollar. Innehållet ligger i fem helskärmslager staplade i ett fast element ovanpå. Scrollpositionen blir ett tal, djupet, från 0 högst upp till 4 längst ner, och en scrollhanterare gör om det talet till transformer.

Mellan två djup är exakt två lager synliga. Det utgående lagret har ett mål, saken du är på väg in i: på djup 0 är det rutan runt namnet, på djup 2 den lilla etiketten ovanför en fras. Det inkommande lagret har en innehållsruta, blocket som sitter mitt på skärmen i sin naturliga storlek. Båda mäts en gång per layout. Allt annat följer av ett enda förhållande:

const s = target.width / incoming.width; // hur stor del av det inkommande lagret som ryms i målet
const k = 1 / s;

// p går från 0 till 1 mellan de två djupen
out.style.transform = `translate(${(cx - tx) * p}px, ${(cy - ty) * p}px) scale(${k ** p})`;
inc.style.transform = `translate(${(tx - cx) * (1 - p)}px, ${(ty - cy) * (1 - p)}px) scale(${s ** (1 - p)})`;

Det utgående lagret växer runt sitt mål och skjuter målet mot mitten. Det inkommande lagret börjar krympt inuti målet och växer till full storlek medan det glider åt samma håll. För varje värde på p upptar målet och det inkommande innehållet samma rektangel, så överlämningen är sömlös. Talet i övre vänstra hörnet är produkten av alla förhållanden så långt, vilket är varför det står något i stil med 330× på botten. Det är inte dekoration, det är uppmätt.

Vad jag gjorde fel först

Min första version tonade över de två lagren under mittenhalvan av varje övergång. Det såg ut som en röra, för resten av det utgående lagret, meningen runt frasen eller det stora namnet ovanför meningen, växte sig enorm bakom det inkommande innehållet. Lösningen var självklar när jag väl såg den: omgivningen måste falla bort först. Allt i det utgående lagret som inte ligger på vägen till målet tonar bort under den första tredjedelen av övergången. Bara målet överlever in i övertoningen, och själva övertoningen blev kortare. Eames-filmen gör samma sak. Man ser aldrig picknickfilten när man väl är bland galaxerna.

Delarna ingen ser

Lagren är vanlig HTML med riktiga rubriker, så sökmotorer och skärmläsare får ett vanligt dokument. Om ditt system ber om reducerad rörelse körs skriptet aldrig, och samma uppmärkning lägger sig platt. Piltangenterna stegar mellan djupen, Escape tar dig tillbaka till ytan, och lagren som inte är framför är markerade inerta så att tabbtangenten inte kan vandra in i något osynligt. Det finns också en platt version på /sv/all för den som har bråttom.

Alltihop är ungefär tvåhundra rader TypeScript, inget animationsbibliotek, inget ramverk i webbläsaren. Astro renderar sidorna, Cloudflare serverar dem. Är du nyfiken på någon detalj når adressen längst ner i zoomen en människa.