Design Lounge
Install

Nº 293 of 520

Lens card scroll ticker

Scroll moves a row of huge benefit lines sideways through a fixed centre card. Inside the card the text turns ink and the card recolours per benefit.

Preview

Open full size
1280 × 800 · 11.0 KB · 210 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 "Lens card scroll ticker" (piece scroll-lens-card-ticker) 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

182 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 wickret.cuberto.com: the benefits strip where one line of big text slides sideways behind a fixed rounded card in the middle of the screen, and the card shows a new picture for each line. This rebuild is for Fernly, a fictional houseplant care membership, on a charcoal page. Vertical scroll drives four benefit lines across the viewport, one at a time. The centre card works like a lens. Text outside it is bone, and the same text seen through it is ink on the card’s colour. As each line settles in the card, the card recolours (sage, clay, sky, mustard), crossfades its line icon, and updates a counter and two small facts. The detail worth copying is the second, clipped copy of the track inside the card, offset by the card’s position, so the colour change lines up exactly at the card edge without any blend modes.

Reference behaviour

  1. First frame: a charcoal stage #161514 pinned to the viewport. Fernly mark top-left and an outlined “Start a membership” pill top-right. Eyebrow “THE MEMBERSHIP” at top-left, with the italic serif line “Four things you will never have to think about.” under it. In the centre, a 280×360 sage card with radius 28. “Watered on your schedule” is centred on it at 64px, bone outside the card and ink inside. A four-option rail sits at the bottom centre, with “Watering” current. A small “SCROLL” cue with a dripping line sits bottom-right.
  2. The page is 520vh tall. The stage is position: sticky; top: 0; height: 100vh, so the stage stays and scroll progress p (0 → 1) drives the track.
  3. The track’s x is chosen so the centre of a phrase sits at the viewport centre. Between phrase i and i+1, the centre is interpolated with smoothstep t*t*(3-2t), so each phrase lingers in the card and then moves on.
  4. The displayed x eases toward the target each frame (cur += (target - cur) * 0.14) for inertia. It stops when within 0.3px.
  5. The active index is round(p × 3). On change: card background crossfades over 500ms, the icon swaps (old fades out, new scales 0.92 → 1 and rotates −4° → 0 over 600ms expo-out), the counter reads “02 / 04”, the bottom facts update, the rail moves aria-current="step", and the live region announces the phrase.
  6. The four phrases and facts are: “Watered on your schedule” (Weekly visit · Included, sage #B9CBA4); “Repotted when it outgrows” (Soil + pot · Included, clay #E79B72); “Swap any plant, any month” (1 swap / month · Free, sky #A9C8DE); “A botanist on call” (Reply in 2 h · 7 days, mustard #E8C35A).
  7. Clicking a rail option scrolls to that phrase’s progress (maxScroll × i / 3) with smooth scroll. Left and right arrow keys on a focused rail option move to the neighbour and focus it.
  8. Resizing re-measures phrase centres and the inner track offset.
  9. Reduced motion: no inertia (x snaps to target), no card or icon transitions, no drip. Rail clicks jump.

Structure

1280 × 800 viewport; document 520vh; stage sticky 100vh
┌──────────────────────────────────────────────────────────────────────┐
│ ⚘ Fernly                                        ( Start a membership )│ 28px / 48px
│ THE MEMBERSHIP                                                        │ top 120
│ Four things you will                                                  │
│ never have to think about.        ┌──────────┐                        │
│                                   │01 / 04   │ 280×360 r28            │
│ Watered on your ░░░░░░░░░░░░░░░░░░│ on your  │ schedule        Re…    │ 64px track, y = 50%
│ (bone)                            │ (ink)    │ (bone)                 │
│                                   │WEEKLY  INCL│                      │
│                                   └──────────┘                        │
│             [ Watering | Repotting | Swaps | Botanist ]        | SCROLL│ bottom 40
└──────────────────────────────────────────────────────────────────────┘
  • div.stage (sticky) holds header.top, div.head (p + h2), div.track.out, div.card, nav.rail, div.cue, and p.sr[aria-live].
  • div.track.out is aria-hidden, absolute, top: 50%, display: flex, white-space: nowrap. Each phrase is a span with margin-right: 220px.
  • div.card is aria-hidden, absolutely centred, overflow: hidden. It contains span.num, four div.art (one inline SVG each), div.track.in (the same phrases), and div.tag with two spans.
  • nav.rail holds four buttons whose visible label is the short name and whose aria-label is the full phrase.

Tokens

:root {
  --bg: #161514;        /* stage, card ink text */
  --bone: #EDE6D8;      /* outside text, active rail fill */
  --bone-2: #A8A194;    /* eyebrow, inactive rail, cue */
  --line: #2C2A27;      /* rail border, cue track */
  --ink: #161514;       /* text inside the card */
  --sage: #B9CBA4;      /* benefit 1 */
  --clay: #E79B72;      /* benefit 2 */
  --sky: #A9C8DE;       /* benefit 3 */
  --mustard: #E8C35A;   /* benefit 4, focus ring */
  --sans: "Bricolage Grotesque", system-ui, sans-serif;
  --serif: "Instrument Serif", Georgia, serif;
  --card-w: 280px; --card-h: 360px; --r-card: 28px;
  --phrase: 64px; --gap: 220px;
  --ease: cubic-bezier(.2,.7,.2,1);
  --expo: cubic-bezier(.16,1,.3,1);
}

Spacing follows 4/8: 6px rail padding, 16px rail button padding, 20px card insets, 28px and 48px page insets.

Typography

RoleFamilySizeWeightLine-heightTrackingCase
Track phraseBricolage Grotesque64px6001−0.035emSentence
Section lineInstrument Serif italic34px4001.1−0.01emSentence
Card counterInstrument Serif italic22px40010“01 / 04”
BrandBricolage Grotesque22px7001−0.03emTitle
Eyebrow / cueBricolage Grotesque13px40010.12–0.14emUpper
Card factsBricolage Grotesque12px60010.1emUpper
Rail / pillBricolage Grotesque14px / 15px60010Title

Motion

ThingTriggerPropertyFrom → toDuration / easingReduced motion
Track (both copies)scrolltranslateXphrase i centre → phrase i+1 centre, smoothsteprAF lerp 0.14 per framesnap
Card colouractive changebackground-colorold → new benefit colour500ms --easeinstant
Icon outactive changeopacity, transform1 → 0360ms --easeinstant
Icon inactive changeopacity; scale + rotate0 → 1; 0.92 / −4° → 1 / 0360ms; 600ms expoinstant
Rail currentactive changebackground, colourtransparent → bone240ms --easeinstant
Scroll cuelooptranslateY of 12px tick−12 → 40px1.8s --ease infinitestatic
Rail clickclickwindow scrollcurrent → phrase positionsmoothjump

States

  • Rail option resting: --bone-2 text. Hover: --bone. Current: bone pill with ink text and aria-current="step".
  • “Start a membership” resting: 1px --bone-2 outline. Hover: bone fill, ink text.
  • Focus-visible: 2px mustard outline, 3px offset, 8px radius on everything.
  • Between phrases, the card shows the gap, so only part of a word or none sits inside it. This is expected and is the point of the lens.
  • No loading or empty states. With fewer than two items, drop the piece and use a static feature block.

Accessibility

  • Both tracks and the card are aria-hidden. The phrases are decorative motion. The rail buttons carry the full phrase in aria-label, and a polite live region announces the active phrase.
  • The rail is a nav labelled “Membership benefits”. Tab reaches the pill, then the four rail options. Arrow keys move between rail options and scroll the page to match.
  • Keyboard users who never scroll can still read all four benefits through the rail.
  • Contrast: bone #EDE6D8 on #161514 is about 15:1. Ink on each card colour is above 7:1 (sage about 10:1, clay about 7.6:1, sky about 10:1, mustard about 10.5:1).
  • Rail buttons are 40px tall, and the pill is 44px.
  • The page needs real scroll height. Do not hijack the wheel. Native scroll drives everything, so trackpads, keyboards (Space, Page Down), and screen readers all work.

Responsive rules

  • ≥1280: as specified.
  • 1024: the same. The card stays 280×360. Phrases still run past both edges.
  • ≤900: phrase 44px, gap 140px, card 220×290, page insets 20px, section line 26px, scroll cue hidden, rail buttons 10px padding.
  • <480: phrase 36px, gap 110px, card 190×250, icon 150px, card facts 10px and no wrap, rail bottom 24px, rail buttons 9px padding and 12px text.
  • The stage is overflow: hidden, so the long track never causes horizontal page scroll. Verify scrollWidth === innerWidth at 375.

Acceptance checklist

Always

  • The stage is sticky for the full section height, and native vertical scroll drives the horizontal track.
  • There are two copies of the track: one outside, one inside the card. The inside copy is offset by the card’s left and top so the copies line up to the pixel.
  • Each phrase dwells in the card (smoothstep between centres), not a constant linear slide.
  • The card colour, icon, counter, and facts change when the active index changes, and only then.
  • The rail shows the current step with aria-current="step". Click and arrow keys scroll to a step.
  • Decorative tracks are aria-hidden, and a live region announces the active phrase.
  • Reduced motion snaps x and removes the card and icon transitions.
  • No horizontal page overflow at 375px.

This demo

  • The page is #161514, outside text bone #EDE6D8, inside text ink.
  • Phrases are 64px Bricolage Grotesque 600, with a 220px gap.
  • The card is 280×360, radius 28, centred, colours sage → clay → sky → mustard.
  • The counter reads “01 / 04” in Instrument Serif italic.
  • The rail reads Watering, Repotting, Swaps, Botanist.
  • The document is 520vh, and the lerp factor is 0.14.

Implementation notes

1. Align the inner copy once, then move both with the same transform. The inner track lives inside the card’s overflow: hidden. Shift it by the card’s offset so its coordinate system matches the stage.

function measure() {
  centers = [...out.children].map(s => s.offsetLeft + s.offsetWidth / 2);
  inn.style.left = -card.offsetLeft + 'px';                    // card has margin-left:-w/2
  inn.style.top  = stage.offsetHeight / 2 - card.offsetTop + 'px';
}
function paint(x) {
  const t = `translate3d(${x}px,-50%,0)`;
  out.style.transform = t;
  inn.style.transform = t;                                      // identical, not offset
}

2. Map progress to x with a dwell. Linear interpolation makes the text never rest. Smoothstep the fraction between neighbouring phrase centres.

const smooth = t => t * t * (3 - 2 * t);
function xFor(p) {
  const f = p * (n - 1), i = Math.min(n - 2, Math.floor(f));
  const k = smooth(Math.min(1, Math.max(0, f - i)));
  return innerWidth / 2 - (centers[i] + (centers[i + 1] - centers[i]) * k);
}

3. Re-measure after fonts load. Phrase widths change when Bricolage swaps in. Call measure() inside document.fonts.ready.then(...) and on resize.

Common mistakes:

  • Using mix-blend-mode: difference for the colour change. It gives the wrong colour on four different card backgrounds and breaks on Safari compositing.
  • Adding half the card width to the inner offset. offsetLeft already includes the negative margin.
  • Hijacking the wheel with preventDefault instead of using a tall document and a sticky stage.
  • Updating the card on every scroll frame instead of only when the rounded index changes.
  • Forgetting white-space: nowrap, so long phrases wrap and the centres are wrong.

Details

Palette
Type
Bricolage Grotesque · Instrument Serif
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