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
Use it
Free to use · credit appreciated1 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.
<!-- Design Lounge Nº 066 · "Sticky split scroll story" · designlounge.vercel.app -->
# Sticky split scroll story
> **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).
… 148 more lines. Copy or download for the full brief. Loading the source…Brief
188 linesBuild 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
- 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”. - Scroll until chapter two’s box overlaps the middle 10 % band of the viewport:
data-chbecomes 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”. - 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”.
- Chapter four: daylight gradient, small pale disc high-right at 60 %, slit 0°, HUD “12:00 · Noon · Dome closed”, accent sage.
- Scrolling back up reverses every step; transitions run in both directions.
- Click a rail number: the page scrolls smoothly to that chapter and the state is set immediately (so the visual leads the scroll).
- 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;.introline; four<section class="ch" id="cN" data-n="N">each with.kkicker,<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.layergradient divs,.stars,.disc,.ground(clip-path ridge),.dome(with::afterslit),.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
| Role | Family | Size | Weight | Line-height | Tracking | Case |
|---|---|---|---|---|---|---|
| Chapter title | Cormorant Garamond | 52px | 500 | 1.02 | −0.015em | sentence, max 14ch, text-wrap: balance; <em> italic 500 in --accent |
| Body | Karla | 16px | 400 | 1.6 | 0 | --ink-2, max 44ch |
| Kicker | Karla | 12px | 500 | 1 | +0.16em | UPPERCASE --accent |
| Figure | Cormorant Garamond | 30px | 600 | 1 | 0 | numerals; caption Karla 11px 500 +0.10em UPPERCASE --ink-3 |
| Intro line | Karla | 12px | 500 | 1 | +0.12em | UPPERCASE; product name in --ink |
| Rail number | Karla | 12px | 500 | 1 | +0.08em | “01”–“04” |
| HUD | Karla | 12px | 500 | 1 | +0.10em | UPPERCASE; time in --ink, rest --ink-2 |
Motion
| Element | Trigger | Property | From → To | Duration | Easing |
|---|---|---|---|---|---|
.layer.lN | data-ch change | opacity | 1 → 0 (old) and 0 → 1 (new), simultaneously | 800ms | --ease |
.stars | ch 2 in / out | opacity | 0 ↔ .9 | 800ms | --ease |
.disc | data-ch change | transform | ch1 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 |
.disc | data-ch change | background, box-shadow | accent ↔ --moon ↔ --sun-noon; glow 80px/10px → 60/6 → 90/20 → 100/30 | 800ms | --ease |
.dome::after | data-ch change | rotate | −40° → −70° → −20° → 0° | 1000ms | --ease |
.k, h2 em, rail marker | data-ch change | color | previous accent → new accent | 800ms / 160ms (rail) | --ease |
body | data-ch change | background | unchanged by default; hook available for tinting | 800ms | --ease |
| rail click | click | scroll | smooth 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):--accenttext and border. Focus-visible: 2px--accentoutline, 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-2on--bg9.6:1;--ink-3on--bg4.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 usesmix-blend-mode: screenso 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 %--bgbackdrop 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 todata-chon<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-chand 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
- Tags
Studied
Haunted Boulder CityPin the screen and let scroll turn the pages. Only the line you are on gets to shout.
All sourcesOn the shelves