Hoppa till innehållet

18 oktober 2026 · 5 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. Sedan jag först skrev den har sidan vuxit ett steg åt varje håll, så siffrorna nedan är de nuvarande.

Ismail Safwat 1× Ismail Safwat 4× Wexiödisk, 2023 → 20× Wexiödisk · 2023 → Mjukvaruingenjör en bit av koden ↓ 80× s = mål.w / nästa.w k = 1 / s FÖRSTORING MOT SCROLL · log 1× 10× 100× 1000× 0 1 2 3 4 · djup ≈ 330× out: translate((cx−tx)·p, (cy−ty)·p) scale(k^p) in: translate((tx−cx)·(1−p), …) scale(s^(1−p)) omgivningen tonar 0.03–0.28 · målet 0.28–0.50 · nästa 0.44–0.66 prefers-reduced-motion → samma dokument, platt 1 2 3 4 5
Fig. 01 — Startsidan som ramar i varandra, fem av dess sju. Var och en är nästa, förstorad; grafen är förstoringen du ser i hörnet.
  1. 1Mål · rutan nästa djup ryms i
  2. 2Inkommande innehåll · mätt en gång per layout
  3. 3Överlämning · omgivningen tonar först, målet överlever
  4. 4Skalavläsning · produkten av de uppmätta förhållandena
  5. 5Platt reserv · samma dokument utan zoomen

Sju lager, en scroll

Dokumentet självt är sju tomma block, vart och ett en skärmhöjd högt. Det är allt webbläsaren scrollar. Innehållet ligger i sju helskärmslager staplade i ett fast element ovanpå. Scrollpositionen blir ett tal, djupet, från −1 vid kartan till 5 vid maskinen, med 0 vid ytan där sidan öppnas, 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 2 000× vid maskinen, och en kartskala som 1:100 ett steg ovanför ytan. 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.

Ett steg upp, ett steg ner

Den första versionen slutade vid koden. Två saker störde mig med det. En bit kod är inte botten på någonting, den kör på något; och ett namn på en tom sida är inte toppen på någonting heller, det finns någonstans. Så sidan går nu ett steg längre åt varje håll. Scrolla upp från namnet och det krymper till en punkt på en karta över södra Vättern, ritad från OpenStreetMaps geometri i sju pixlar per kilometer, med Jönköping vid sjöns spets. Scrolla ner förbi koden och du når maskinen den kör på: en huvdiskmaskin, en telefon på en fönsterbräda, en laptop med fem processer som låtsas vara fem maskiner. Under det finns bara människor, och det säger sidan och stannar.

Koden själv kör medan du tittar på den. En rad i taget tänds, med ett ord bredvid som säger vilket tillstånd maskinen är i, och för diskmaskinen tar stegen lika lång tid som i ett riktigt program, med en klocka. Sidan minns också var du lämnade den, öppnas där nästa gång och glömmer så fort du går tillbaka till ytan. Tryck P så läggs varje djup ut som ett numrerat blad med titelfält, vilket också är vad sidan skrivs ut som. Det finns ett tick vid varje överlämning om du slår på det, och på kvällen blir papperet en nyans varmare.

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 sexhundra 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.