Design Lounge
Install

Nº 066 of 520

Sticky split scroll story

A two-column scrollytelling layout: four text chapters scroll on the left while a sticky sky illustration on the right crossfades and shifts colour as each chapter enters view.

Preview

Open full size
1280 × 800 · 10.6 KB · 127 lines

Use it

Free to use · credit appreciated

1 Once, in your project

npx skills add SusanAcharya/Design-Lounge

2 Then ask

Using the Design Lounge skill, build "Sticky split scroll story" (piece sticky-split-story) in my stack. Match its brief; restyle it onto my product's theme.

No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.

Brief

188 lines

Build brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.

What it is

A long-form feature (“Aster Observatory · One night on Skarvfjell”) split in two: the left half is four chapters of text, each a full viewport tall, with a numbered rail that tracks progress; the right half is a position: sticky illustration of the sky over the observatory — gradient layers, a sun/moon disc, a ridge line, a small dome — that crossfades and moves as each chapter crosses the middle of the viewport. The page’s accent colour (kickers, italic phrases, rail marker, disc) shifts with the chapter: ochre at dusk, blue-grey at midnight, apricot at dawn, sage at noon. The mechanism worth copying is that all visual state hangs off one attribute, data-ch on <body>, set by an IntersectionObserver with a 45 % inset root margin.

Reference behaviour

  1. Initial state (data-ch="1"): rail marker on “01”, kicker “Chapter one · Dusk” in ochre, headline with an italic ochre phrase, two paragraphs, three serif figures. Right: dusk gradient (violet to copper to ochre), a 180px ochre disc low-right behind the ridge, dome slit tilted −40°, HUD reads “19:42 local” and “Dusk · Sun setting”.
  2. Scroll until chapter two’s box overlaps the middle 10 % band of the viewport: data-ch becomes 2. The dusk layer fades out and the midnight layer fades in (800ms); nine stars fade to 90 %; the disc travels to the upper-left, shrinks to 45 % and turns bone-white (1000ms expo-out); the dome slit rotates to −70°; the accent turns blue-grey; the HUD updates to “00:10 · Midnight · Moon in the west”.
  3. Chapter three: dawn gradient, stars fade out, disc returns low-centre at 80 % in apricot with a wider glow, slit −20°, HUD “04:20 · Dawn · Field low in the east”.
  4. Chapter four: daylight gradient, small pale disc high-right at 60 %, slit 0°, HUD “12:00 · Noon · Dome closed”, accent sage.
  5. Scrolling back up reverses every step; transitions run in both directions.
  6. Click a rail number: the page scrolls smoothly to that chapter and the state is set immediately (so the visual leads the scroll).
  7. The rail (01–04) is sticky at the top-left; the active number is in the current accent with a 2px left bar.

Structure

1280 × 800 (page scrolls; total height ≈ 4 × 100vh + intro)
┌──────────────────────────────────┬─────────────────────────────────────┐
│ 01│ ASTER OBSERVATORY · ONE NIGHT…│                                     │
│ 02│                               │   gradient layer (active chapter)   │
│ 03│  CHAPTER ONE · DUSK           │            stars (ch 2)             │
│ 04│  The dome opens at 19:42.     │                     ● disc          │
│   │  paragraph (44ch)             │        ▲▲  ridge  ▲▲  ◠ dome        │
│   │  paragraph                    │  ────────────────────────────────── │
│   │  19:42   −3.4 °C   61 %       │  19:42 LOCAL          DUSK · SUN…   │
│   │  (next chapter below, 100vh)  │  (sticky, 100vh)                    │
└──────────────────────────────────┴─────────────────────────────────────┘
 left column: padding 0 72px 0 96px; rail at left −60px, top 44px
 right column: 1px left hairline; HUD at 32px insets, 28px from bottom
  • <body data-ch="1"> → .wrap (grid-template-columns: 1fr 1fr; min-height: 100%).
  • .text: .rail (position: sticky; top: 0; height: 0) containing an <ol> of four anchor links; .intro line; four <section class="ch" id="cN" data-n="N"> each with .k kicker, <h2> with <em>, two <p>, and .fig (three figures with <small> captions).
  • <aside class="visual" aria-label="…"> (position: sticky; top: 0; height: 100vh; overflow: hidden): four .layer gradient divs, .stars, .disc, .ground (clip-path ridge), .dome (with ::after slit), .hud.

Tokens

:root {
  /* colour — warm near-black page, bone text, chapter accents */
  --bg: #14110f;
  --ink: #efe9df;
  --ink-2: #b7ae9f;      /* body copy */
  --ink-3: #77705f;      /* rail numbers, captions */
  --line: #2b2622;       /* hairlines */
  --c1: #d9a441;         /* dusk — ochre */
  --c2: #8fa7c9;         /* midnight — blue-grey */
  --c3: #e28a5a;         /* dawn — apricot */
  --c4: #8fae8b;         /* noon — sage */
  --accent: var(--c1);   /* re-pointed by [data-ch] */
  --moon: #e9e4d6; --sun-noon: #fff5d6; --ground: #0f0d0b;
  /* sky layers (top → bottom stops) */
  --sky-1: linear-gradient(180deg, #3a2b3f 0, #8a4a3a 55%, #d9a441 100%);
  --sky-2: linear-gradient(180deg, #05070f 0, #101a2e 60%, #1d2b45 100%);
  --sky-3: linear-gradient(180deg, #2c3a55 0, #b06a4a 60%, #f0c28a 100%);
  --sky-4: linear-gradient(180deg, #6f9fc9 0, #a9c9dc 60%, #dfe6d0 100%);

  /* type */
  --serif: "Cormorant Garamond", Georgia, serif;
  --sans: "Karla", system-ui, sans-serif;
  --fs-h2: 52px; --fs-body: 16px; --fs-fig: 30px; --fs-kicker: 12px; --fs-rail: 12px; --fs-hud: 12px;
  --measure: 44ch; --h2-measure: 14ch;

  /* layout */
  --text-pad: 0 72px 0 96px; --ch-pad: 96px 0; --rail-x: -60px; --rail-y: 44px;
  --disc: 180px; --dome-w: 64px; --dome-h: 40px; --ground-h: 34%;
  --spy-inset: 45%;   /* IntersectionObserver rootMargin top/bottom */

  /* motion */
  --t-micro: 160ms; --t-fade: 800ms; --t-move: 1000ms;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);
}
[data-ch="2"] { --accent: var(--c2); }
[data-ch="3"] { --accent: var(--c3); }
[data-ch="4"] { --accent: var(--c4); }

Typography

RoleFamilySizeWeightLine-heightTrackingCase
Chapter titleCormorant Garamond52px5001.02−0.015emsentence, max 14ch, text-wrap: balance; <em> italic 500 in --accent
BodyKarla16px4001.60--ink-2, max 44ch
KickerKarla12px5001+0.16emUPPERCASE --accent
FigureCormorant Garamond30px60010numerals; caption Karla 11px 500 +0.10em UPPERCASE --ink-3
Intro lineKarla12px5001+0.12emUPPERCASE; product name in --ink
Rail numberKarla12px5001+0.08em“01”–“04”
HUDKarla12px5001+0.10emUPPERCASE; time in --ink, rest --ink-2

Motion

ElementTriggerPropertyFrom → ToDurationEasing
.layer.lNdata-ch changeopacity1 → 0 (old) and 0 → 1 (new), simultaneously800ms--ease
.starsch 2 in / outopacity0 ↔ .9800ms--ease
.discdata-ch changetransformch1 translate(120px,150px) scale(1) → ch2 translate(−160px,−180px) scale(.45) → ch3 translate(−40px,120px) scale(.8) → ch4 translate(90px,−220px) scale(.6)1000ms--ease-out
.discdata-ch changebackground, box-shadowaccent ↔ --moon ↔ --sun-noon; glow 80px/10px → 60/6 → 90/20 → 100/30800ms--ease
.dome::afterdata-ch changerotate−40° → −70° → −20° → 0°1000ms--ease
.k, h2 em, rail markerdata-ch changecolorprevious accent → new accent800ms / 160ms (rail)--ease
bodydata-ch changebackgroundunchanged by default; hook available for tinting800ms--ease
rail clickclickscrollsmooth scroll to chapter (scroll-behavior: smooth on html)native—

Reduced motion: all transitions 1ms (state still switches), scroll-behavior: auto.

States

  • Chapter active: determined only by data-ch; the text sections themselves do not change style, the rail and visual do.
  • Rail link rest: --ink-3, transparent 2px left border. Hover: --ink-2. Active (.on): --accent text and border. Focus-visible: 2px --accent outline, 2px offset.
  • Disc per chapter: see the transform table; behind the ridge in chapters 1 and 3 (DOM order: disc before ground).
  • HUD: text swaps instantly at the state change (<b id="hudTime">, <span id="hudLabel">).
  • No loading, empty or error states.

Accessibility

  • The visual is an <aside aria-label="Sky above the observatory, changing with each chapter">; all its children are decorative and carry no text except the HUD, which is real text.
  • Chapters are <section>s with <h2>; the rail is an <ol> of <a href="#cN"> so it works without JS and appears in the tab order before the chapters.
  • The IntersectionObserver only sets data-ch; nothing important is conveyed by colour alone (each chapter’s kicker names the time of day, the HUD names it again).
  • Contrast: --ink-2 on --bg 9.6:1; --ink-3 on --bg 4.6:1 (used at 11–12px 500 uppercase); accents on --bg: ochre 8.6:1, blue-grey 7.9:1, apricot 7.2:1, sage 7.6:1. HUD uses mix-blend-mode: screen so it stays legible on the light noon sky (ink on #dfe6d0 ≈ 1.2:1 without the blend — if blend modes are unavailable, give the HUD a 60 % --bg backdrop pill).
  • Smooth scrolling and all transitions respect prefers-reduced-motion.

Responsive rules

  • ≥ 1280: two equal columns; chapters min-height: 100vh.
  • 1024–1279: same; headline 44px; left padding 64px.
  • 768–1023: single column; the visual moves to the top (order: -1), 44vh tall, sticky; text scrolls beneath it; chapters lose the 100vh minimum and use 64px vertical padding; rail sits at left −36px below the visual.
  • < 640: visual 36vh; headline 34px; figures wrap; rail hidden (chapters still have ids for in-page links).

Acceptance checklist

  • Layout is a two-column grid; the right column is position: sticky; top: 0; height: 100vh; overflow: hidden.
  • Each chapter section is at least 100vh tall with 96px vertical padding and content vertically centred.
  • The active chapter is tracked with an IntersectionObserver using rootMargin: "-45% 0px -45% 0px" and written to data-ch on <body>.
  • Sky gradients crossfade by opacity over 800ms; only one layer is at opacity 1 at rest.
  • The disc moves and scales between the four listed transforms over 1000ms with cubic-bezier(.16,1,.3,1) and changes colour over 800ms.
  • Stars are visible only in chapter 2 (opacity .9).
  • The accent custom property changes with data-ch and recolours kickers, italic phrases, rail marker, disc and dome slit.
  • HUD time and label update to the chapter’s values.
  • Rail numbers are links that scroll to their chapter and become active immediately on click.
  • Scrolling back up restores the previous chapter’s visual.
  • Headline is Cormorant Garamond 52px/1.02 with a max measure of 14ch; body is Karla 16px/1.6 at 44ch.
  • With reduced motion, chapters still switch the visual, instantly.

Implementation notes

One attribute drives everything. Observe the chapters with a root margin that leaves only the middle 10 % of the viewport, so exactly one chapter can be “current” at a time:

const io = new IntersectionObserver(entries => {
  entries.forEach(e => { if (e.isIntersecting) set(e.target.dataset.n); });
}, { rootMargin: '-45% 0px -45% 0px', threshold: 0 });
document.querySelectorAll('.ch').forEach(c => io.observe(c));
function set(n) {
  if (document.body.dataset.ch === String(n)) return;
  document.body.dataset.ch = n;
  links.forEach(a => a.classList.toggle('on', a.dataset.n === String(n)));
}

Crossfade gradients with stacked layers, because background-image gradients cannot transition:

.layer { position: absolute; inset: 0; opacity: 0; transition: opacity var(--t-fade) var(--ease); }
[data-ch="1"] .l1, [data-ch="2"] .l2, [data-ch="3"] .l3, [data-ch="4"] .l4 { opacity: 1; }
.disc { transition: transform var(--t-move) var(--ease-out), background var(--t-fade) var(--ease); }
[data-ch="2"] .disc { transform: translate(-160px, -180px) scale(.45); background: var(--moon); }

Ridge with clip-path — one element, no SVG, and the disc sits behind it by DOM order:

.ground { position: absolute; left: 0; right: 0; bottom: 0; height: 34%; background: var(--ground);
  clip-path: polygon(0 38%, 18% 30%, 34% 40%, 52% 24%, 68% 36%, 84% 22%, 100% 34%, 100% 100%, 0 100%); }

Common mistakes: putting overflow: hidden on an ancestor of the sticky column (sticky silently stops working); using threshold: .5 instead of a root margin (tall chapters never reach 50 % visibility on short viewports); transitioning background-image (it snaps); animating top/left on the disc instead of transform.

Details

Palette
Type
Cormorant Garamond · Karla
Motion
Rich motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Haunted Boulder City

Pin the screen and let scroll turn the pages. Only the line you are on gets to shout.

All sources