Design Lounge
Install

Nº 186 of 520

Case file horizontal scroll

A pinned section where scrolling down slides three foggy night 'case file' panels sideways, with chapter tabs that fill and a 01 / 03 counter.

Preview

Open full size
1280 × 800 · 13.7 KB · 188 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 "Case file horizontal scroll" (piece case-file-horizontal-scroll) 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

206 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

Studied from hauntedbouldercity.com: the “strange stuff” section, where vertical scrolling pins the screen and slides three full-height story panels sideways, each a night scene with a huge condensed headline, while tabs at the bottom track the chapter. This piece is that section for a fictional harbour ghost walk, the Wrexley Lantern Walk. Three case files (the keeper, the wreck, the lights) sit side by side on a track. The next panel always peeks in from the right at 22% width, dimmed, so people know to keep going. The detail worth copying is that every piece of chrome reads the same single number, scroll progress from 0 to 1: the track position, the tab fills, the counter, the dimming and the top progress bar.

Reference behaviour

  1. First frame: panel 01 fills the left 78% of the screen. Panel 02 peeks on the right, its copy at 35% opacity. Tab “01 Keepers” is current and its top rule is lit amber. The counter reads 01 / 03. Fog drifts slowly across the bottom.
  2. Scrolling down does not move the page visibly. The section is pinned (position: sticky) and the track translates left in exact proportion to scroll. There is no snapping and no easing; the track follows the wheel.
  3. The pinned scroll distance is 240vh (section 340vh minus one screen). Across it, the track moves by trackWidth - viewportWidth.
  4. Each tab’s 1px top rule fills left to right as you pass through its chapter (scaleX 0 → 1). Tabs behind you stay full.
  5. When progress × 2 rounds to a new integer, that panel becomes current: its copy fades to full opacity (500ms), the previous one dims to 35%, the tab gets aria-current="true", the counter changes, and the live region says “Case file 2 of 3”.
  6. Clicking a tab smooth-scrolls the window to that chapter’s exact progress point (0, 0.5, 1). Under reduced motion it jumps.
  7. ArrowLeft / ArrowRight step one chapter while the section is fully pinned on screen.
  8. A 3px amber bar at the very top shows whole-page progress.
  9. After the last panel, the section un-pins and a short “Pick a foggy night.” booking block scrolls up.

Structure

1280 × 800, section height 340vh, pinned frame 100vh
┌──────────────────────────────────────────────────────────────┬────────────┐
│▀▀▀▀▀▀ 3px page progress                                       │            │
│ WREXLEY LANTERN WALK                        [Book a lantern ↗]│            │ fixed header, 22px 48px
│ 02 / THE HARBOUR FILES          KEEP SCROLLING. IT GETS COLDER│            │ meta row, top 74px
│                                     ( moon )                  │  next      │
│ CASE FILE 01 / THE KEEPER                                     │  panel     │
│ THE LIGHT                              ▲ lighthouse           │  peeks     │
│ STAYED ON                                                     │  (22vw,    │
│ FOR NOBODY.          (amber line)                  ┌──┐       │  copy at   │
│ paragraph max 420px                                │01│ outline│  35%)      │
│ KEEPERS & LOGBOOKS                                 └──┘       │            │
│ ~~~~~~~~~~~~~~~~~~~~~ fog ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~│            │
│ ─01 Keepers─  ─02 Wrecks─  ─03 Lights─                01 / 03 │            │ foot, bottom 30px
└──────────────────────────────────────────────────────────────┴────────────┘
 panels: 78vw, 78vw, 100vw (last one fills the screen at progress 1)
  • div.bar fixed, aria-hidden.
  • header.top fixed: brand text, booking link.
  • section.wrap aria-label="Case files" (340vh) → div.pin (sticky, 100vh, overflow hidden) → .meta, .track (flex row), .foot.
  • Each panel is an article aria-labelledby its h2. Layers inside, back to front: .sky gradient, .moon, inline SVG scene silhouette (preserveAspectRatio="xMidYMax slice", 62% height), two .fog layers, .shade left-to-right darkening gradient, .num outlined numeral, .copy (kicker, h2, p, tag).
  • .foot: ol.tabs aria-label="Case files" of three buttons, and the counter (aria-hidden; the live region speaks).
  • section.after with an h3 and a second booking link.

Tokens

:root {
  --night: #0a1220;          /* page */
  --night-2: #111c30;
  --deep: #060b14;           /* silhouettes, button text */
  --bone: #ece6d8;           /* headlines */
  --bone-2: #b9b7ae;         /* body, meta */
  --bone-3: #8d9099;         /* tags, inactive tab numbers */
  --line: rgba(236,230,216,.18);
  --fog: #9fb4c4;            /* fog gradients at 26–34% alpha */
  --lamp: #e8873a;           /* lantern amber: highlight line, button, progress */
  --lamp-2: #f2b27a;         /* lit window, kicker bold, current tab text */
  --focus: #f2b27a;

  --display: "Anton", Impact, "Arial Narrow", sans-serif;
  --mono: "DM Mono", ui-monospace, monospace;

  --h2: clamp(44px, min(11vh, 13vw), 104px);
  --numeral: min(40vh, 330px);
  --gutter: 48px;            /* 18px under 760px */
  --panel: 78vw;             /* 88vw under 760px; last panel 100vw */
  --pin-length: 340vh;

  --ease: cubic-bezier(.2,.7,.2,1);
  --expo: cubic-bezier(.16,1,.3,1);
  --t-dim: 500ms; --t-copy: 700ms;
  --fog-a: 38s; --fog-b: 54s;
}

Sky gradients per panel (top → bottom): #0d1a2e → #1b2c44 → #2a3a52, #081322 → #12253a → #1e3a4a, #05101d → #0f2033 → #1a2e45.

Typography

RoleFamilySize / LHWeightTrackingCase / colour
BrandAnton20px4000.04emuppercase, middle word --lamp
Meta rowDM Mono11px4000.14emuppercase, --bone-2; right item --lamp-2
KickerDM Mono11px400 / 5000.16emuppercase; “Case file 01” --lamp-2
Headline h2Anton--h2 / 0.924000.005emuppercase, 3 short lines, last line --lamp
BodyDM Mono14.5px / 1.554000--bone-2, max 420px
TagDM Mono11px4000.16emuppercase, --bone-3
TabDM Mono13px4000number --bone-3, current --lamp
CounterDM Mono12px400 / 5000.2emtabular, current number --bone
NumeralAnton--numeral / 0.84000transparent fill, 1.5px stroke at 22% bone
ButtonDM Mono13px5000.02emsentence case, --deep on --lamp

Each headline is three lines broken by hand with <br>. Never let a condensed headline wrap on its own.

Motion

ThingTriggerPropertyMapping / from → toTimingReduced motion
TrackscrolltranslateX-p × (trackWidth − innerWidth)1:1 with scroll, rAFkept (user-driven)
Tab rulescrollscaleX from leftclamp(p×(n−1) − i + 1, 0, 1)1:1kept
Page barscrollscaleXpage scroll / max1:1kept
Panel copychapter changeopacity0.35 ↔ 1500ms --easeinstant
Fog A / BalwaystranslateX-6% → 8%, alternate38s / 54s linearstatic
Tab clickclickwindow scrollto top + range × i/(n−1)browser smoothjump
Button arrowhovertranslate0 → (2px, -2px)200ms exponone

Scroll-linked transforms stay under reduced motion because the user is driving them. Only the ambient fog stops.

States

  • Tab resting: --bone-2 text, 1px --line top rule, amber fill at its current --f.
  • Tab current: aria-current="true", text --lamp-2, number --lamp.
  • Tab hover: text --bone. Focus-visible: 2px --focus outline at 6px offset.
  • Panel current: .on, copy full opacity. Others: copy 35%.
  • Button hover: background --lamp-2, arrow nudges up-right.
  • Top of page: tab 01 current, bar at 0. End of section: tab 03 current and full, track at max, then normal scroll resumes.

Accessibility

  • Every panel is a real article with an h2, all in the DOM, so screen readers and find-in-page reach all three without scrolling sideways.
  • Tabs are buttons in an ol with aria-current, not a tablist; they move the scroll position, they don’t hide content.
  • A polite live region announces “Case file N of 3” on chapter change. The counter is aria-hidden to avoid double reading.
  • Keyboard: Tab reaches the booking link and the three tabs. ArrowLeft/Right step chapters only when the section is pinned, so arrows still scroll text elsewhere.
  • Decorative layers (sky, moon, scenes, fog, shade, numeral, bar) are aria-hidden.
  • Contrast: #ece6d8 on #0a1220 ≈ 15:1; #b9b7ae on the shaded left side ≈ 9:1; the .shade gradient (88% → 10% near-black from the left) guarantees this over any scene.
  • Tab hit area: 150px × 40px.

Responsive rules

  • ≥1280: as drawn. Panels 78vw.
  • 1024: same. The headline clamps on height (11vh).
  • 768: same layout; --h2 now clamps on width (13vw).
  • <760: gutters 18px, panels 88vw, tab words hidden (numbers only), meta right item hidden, numeral 34vh, button padding 11px 12px. Pinning still works with touch scroll.
  • Short screens (<600px tall): reduce the pin length to 260vh so the section doesn’t feel endless.
  • If the product has more than five chapters, don’t use this pattern; use a stacked story.

Acceptance checklist

Always

  • The section pins and the track moves sideways 1:1 with vertical scroll; no scroll hijacking and no snapping.
  • The next panel peeks in at ~22% width with dimmed copy.
  • Track, tab fills, counter, dimming and page bar all derive from one progress value.
  • Tabs scroll to their chapter; arrows step chapters while pinned.
  • All panel text is in the DOM and reachable without horizontal scrolling.
  • A shade gradient sits between scene and copy so body text passes 4.5:1.
  • Ambient fog stops under reduced motion; scroll-linked movement stays.
  • No horizontal page overflow at 375px (the track is clipped by the pin).

This demo

  • Section 340vh; panels 78vw, 78vw, 100vw.
  • Headlines: “The light stayed on for nobody.”, “The tide gives things back.”, “Three lights. No boat. No answer.”, last line amber #e8873a.
  • Tabs “01 Keepers”, “02 Wrecks”, “03 Lights”; counter “01 / 03”.
  • Booking button “Book a lantern” amber with near-black text.
  • Outlined numerals 01–03 bottom-right of each panel.

Implementation notes

  1. One progress value drives everything. Throttle with rAF. Compute the track distance from the real scrollWidth, not from panel counts, so mixed panel widths work.
function update() {
  ticking = false;
  const range = wrap.offsetHeight - innerHeight;
  const p = clamp((scrollY - wrap.offsetTop) / range, 0, 1);
  track.style.transform = `translate3d(${-p * (track.scrollWidth - innerWidth)}px,0,0)`;
  const f = p * (n - 1);
  tabs.forEach((t, i) => t.style.setProperty("--f", clamp(f - i + 1, 0, 1)));
  const a = Math.round(f);
  if (a !== active) { active = a; /* toggle .on, aria-current, counter, live region */ }
}
addEventListener("scroll", () => { if (!ticking) { ticking = true; requestAnimationFrame(update); } }, { passive: true });
  1. Pin with sticky, not fixed. The tall wrapper creates the scroll distance; the sticky child stays put. overflow: hidden on the pin clips the track so the page never scrolls sideways.
.wrap  { position: relative; height: 340vh; }
.pin   { position: sticky; top: 0; height: 100vh; overflow: hidden; }
.track { display: flex; height: 100%; will-change: transform; }
.panel { flex: 0 0 78vw; } .panel:last-child { flex-basis: 100vw; }
  1. Tabs jump to exact progress points. top = wrap.offsetTop + range × i / (n − 1); use behavior: "auto" under reduced motion.

Common mistakes:

  • Listening to wheel and translating on your own. That breaks trackpads, keyboards and touch. Map native scroll.
  • Setting overflow-x: hidden only on body; clip at the pin or iOS will still pan sideways.
  • Putting overflow: hidden on an ancestor of the sticky element, which silently breaks sticky.
  • Snapping the track with CSS scroll-snap on a transformed element. It won’t snap and it fights the mapping.
  • Letting long headlines wrap into four or five lines; write three-line headlines.
  • Photo backgrounds with no shade layer; the copy disappears on bright patches.

Details

Palette
Type
Anton · DM Mono
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