Nº 598 of 605
Scroll ring carousel
Ten film posters on a 3D ring that turns 36 degrees per card with scroll, drag or arrow keys, snaps to the front card and swaps the 76px title below.
Preview
Use it
Free to use · credit appreciated1 Once, in your project · Node 22.20+
npx skills add SusanAcharya/Design-Lounge 2 Then ask
Using the Design Lounge skill, build "Scroll ring carousel" (piece scroll-ring-carousel) 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º 598 · "Scroll ring carousel" · www.designlounge.live -->
# Scroll ring carousel
> **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
The competition page of a fictional festival, Northlight Film Week (6 to 15 November 2026, Harbour Cinema, Kessel). Ten posters stand on a 3D ring, each rotated 36° around a vertical axis 470px from its face. The page section is pinned and 46vh of scroll turns the ring by one card; you can also drag the ring sideways, flick it, press Left/Right/Home/End, use the round step buttons or click a side poster. However it moves, the ring settles on a card, the front card is lit at full brightness while its neighbours fall off with the cosine of their angle, and a details row under the ring swaps to the front film's title (76px Big Shoulders ExtraBold), director, runtime and screenings. The detail worth copying: scroll position is the only state. Drag, keys and snapping all write `scrollTo`, and one paint function turns the scroll position into the ring angle, card lighting and the details row, so the inputs can never disagree.
## Reference behaviour
1. Initial frame (1280×800, scrollY 0): fixed 60px top bar ("NORTHLIGHT" in ink and "FILM WEEK" in amber, 22px/900 caps; the dates in dim 16px; Competition (current, amber), Shorts, Passes). A dim hint at (40, 84): "Competition · Scroll, drag or use arrow keys". The ring centred in the space above the details row: front poster "The Salt Year" (280×400, red ground, sun-and-stripes motif) at full brightness, "Last Bus to Ostra" and "Glasshouse" either side at 81% facing, two more cards at the edges at 31%. A faint amber floor glow under the ring. Details row: kicker "Competition · Hungary", title "THE SALT YEAR", a three-row fact list (Director Ilse Varga, Runtime 104 min, Screens Fri 7 Nov 19:30 · Sun 9 Nov 16:00), and on the right an amber "Book seats" pill, step buttons and the counter "01 / 10".
2. Scroll: the section is `100vh + 9 × 46vh` tall with a sticky stage. Fractional index `f = progress × 9`; ring angle `−f × 36°`. Cards light continuously as they pass the front.
3. Snap: 160ms after the last scroll event, if the stage is fully pinned and `f` is more than 0.02 from a whole card, the page smooth-scrolls to the nearest card.
4. Drag (mouse, pen or touch, horizontal): every 220px of pointer travel is one card; the page scrolls instantly to match while dragging. On release it smooth-scrolls to `round(f − vx × 1.2)` where `vx` is the last pointer velocity in px/ms, so a flick carries one or two extra cards. A drag of more than 6px suppresses the click that follows.
5. Keys while the stage has focus: Right = next, Left = previous, Home = first, End = last. Each smooth-scrolls to that card's scroll position.
6. Click a side poster: the ring turns to it. Step buttons do the same as the arrow keys. Ends clamp (no wrap), counter shows "NN / 10".
7. When the front card changes, the details row, the counter, the Book seats label and a polite live region ("Film 6 of 10: Night Shift at Kessel, Mirek Dvorak") update, and the front card gets a 1px 40%-ink ring.
8. Poster type (country · runtime, and the title) is shown only on the front card; every other poster shows its art only, so dimmed type is never rendered as text.
9. "Book seats" changes to "Seats held" for 1.6s on click.
10. After the pin, a "Festival pass, 22 screenings" band follows.
## Structure
```
1280 × 800
┌──────────────────────────────────────────────────────────────────────┐
│ NORTHLIGHT FILM WEEK 6 to 15 November … COMPETITION SHORTS PASSES│ 60 fixed
├──────────────────────────────────────────────────────────────────────┤
│ COMPETITION · SCROLL, DRAG OR USE ARROW KEYS │ stage sticky 100vh
│ ▕▏┌──────┐ ┌──────────┐ ┌──────┐▕▏ │ grid rows 1fr / 200px
│ ▕▏│ −1 │ │ FRONT │ │ +1 │▕▏ ring: 280 × 400 │
│ ▕▏│ 81% │ │ 280×400 │ │ 81% │▕▏ radius 470, 36°/card │
│ ▕▏└──────┘ └──────────┘ └──────┘▕▏ perspective 1700px │
│ ░░░░ floor glow ░░░░ │
│ COMPETITION · HUNGARY DIRECTOR Ilse Varga (BOOK SEATS) │
│ THE SALT YEAR 76px RUNTIME 104 min (‹) 01/10 (›) │
│ SCREENS Fri 7 Nov 19:30 · … │
└──────────────────────────────────────────────────────────────────────┘
… 166 more lines. Copy or download for the full brief. Loading the source…Brief
206 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
The competition page of a fictional festival, Northlight Film Week (6 to 15 November 2026, Harbour Cinema, Kessel). Ten posters stand on a 3D ring, each rotated 36° around a vertical axis 470px from its face. The page section is pinned and 46vh of scroll turns the ring by one card; you can also drag the ring sideways, flick it, press Left/Right/Home/End, use the round step buttons or click a side poster. However it moves, the ring settles on a card, the front card is lit at full brightness while its neighbours fall off with the cosine of their angle, and a details row under the ring swaps to the front film’s title (76px Big Shoulders ExtraBold), director, runtime and screenings. The detail worth copying: scroll position is the only state. Drag, keys and snapping all write scrollTo, and one paint function turns the scroll position into the ring angle, card lighting and the details row, so the inputs can never disagree.
Reference behaviour
- Initial frame (1280×800, scrollY 0): fixed 60px top bar (“NORTHLIGHT” in ink and “FILM WEEK” in amber, 22px/900 caps; the dates in dim 16px; Competition (current, amber), Shorts, Passes). A dim hint at (40, 84): “Competition · Scroll, drag or use arrow keys”. The ring centred in the space above the details row: front poster “The Salt Year” (280×400, red ground, sun-and-stripes motif) at full brightness, “Last Bus to Ostra” and “Glasshouse” either side at 81% facing, two more cards at the edges at 31%. A faint amber floor glow under the ring. Details row: kicker “Competition · Hungary”, title “THE SALT YEAR”, a three-row fact list (Director Ilse Varga, Runtime 104 min, Screens Fri 7 Nov 19:30 · Sun 9 Nov 16:00), and on the right an amber “Book seats” pill, step buttons and the counter “01 / 10”.
- Scroll: the section is
100vh + 9 × 46vhtall with a sticky stage. Fractional indexf = progress × 9; ring angle−f × 36°. Cards light continuously as they pass the front. - Snap: 160ms after the last scroll event, if the stage is fully pinned and
fis more than 0.02 from a whole card, the page smooth-scrolls to the nearest card. - Drag (mouse, pen or touch, horizontal): every 220px of pointer travel is one card; the page scrolls instantly to match while dragging. On release it smooth-scrolls to
round(f − vx × 1.2)wherevxis the last pointer velocity in px/ms, so a flick carries one or two extra cards. A drag of more than 6px suppresses the click that follows. - Keys while the stage has focus: Right = next, Left = previous, Home = first, End = last. Each smooth-scrolls to that card’s scroll position.
- Click a side poster: the ring turns to it. Step buttons do the same as the arrow keys. Ends clamp (no wrap), counter shows “NN / 10”.
- When the front card changes, the details row, the counter, the Book seats label and a polite live region (“Film 6 of 10: Night Shift at Kessel, Mirek Dvorak”) update, and the front card gets a 1px 40%-ink ring.
- Poster type (country · runtime, and the title) is shown only on the front card; every other poster shows its art only, so dimmed type is never rendered as text.
- “Book seats” changes to “Seats held” for 1.6s on click.
- After the pin, a “Festival pass, 22 screenings” band follows.
Structure
1280 × 800
┌──────────────────────────────────────────────────────────────────────┐
│ NORTHLIGHT FILM WEEK 6 to 15 November … COMPETITION SHORTS PASSES│ 60 fixed
├──────────────────────────────────────────────────────────────────────┤
│ COMPETITION · SCROLL, DRAG OR USE ARROW KEYS │ stage sticky 100vh
│ ▕▏┌──────┐ ┌──────────┐ ┌──────┐▕▏ │ grid rows 1fr / 200px
│ ▕▏│ −1 │ │ FRONT │ │ +1 │▕▏ ring: 280 × 400 │
│ ▕▏│ 81% │ │ 280×400 │ │ 81% │▕▏ radius 470, 36°/card │
│ ▕▏└──────┘ └──────────┘ └──────┘▕▏ perspective 1700px │
│ ░░░░ floor glow ░░░░ │
│ COMPETITION · HUNGARY DIRECTOR Ilse Varga (BOOK SEATS) │
│ THE SALT YEAR 76px RUNTIME 104 min (‹) 01/10 (›) │
│ SCREENS Fri 7 Nov 19:30 · … │
└──────────────────────────────────────────────────────────────────────┘
<header class="top">fixed with brand link, date span and<nav aria-label="Festival">.<section class="reel">(tall) with a visually hidden<h1>;.stageisposition: sticky; top: 0; height: 100vh; padding-top: 60px; display: grid; grid-template-rows: 1fr auto,tabindex="0",role="region",aria-roledescription="carousel",aria-label,touch-action: pan-y..scene(perspective: 1700px; perspective-origin: 50% 45%) →.ring(transform-style: preserve-3d) → ten.card(aria-hidden) →.posterwith an SVG motif in the middle 50% and HTML type (<small>,<h3>)..info3-column grid1.3fr 1fr auto, 200px tall: kicker +<h2>;<dl class="facts">;.ctlwith the Book link, prev/next buttons and counter.<p class="sr" aria-live="polite">for announcements.
Tokens
:root {
/* colour */
--bg: #0d0c0b; /* page */
--panel: #171513;
--line: #2b2825; /* bar rule, section rule */
--ink: #f2ebe0; /* text */
--dim: #a59d90; /* labels, dates, hint */
--acc: #f2a33a; /* sodium amber: brand half, current nav, CTA, kicker */
--acc-hover: #f7b55a;
--acc-ink: #1b1206;
--step-line: #6f685e; /* round step buttons */
/* poster grounds / inks */
--p1: #b23a24 / #f4e6cf; --p2: #1f4d45 / #e8efe0; --p3: #e3b341 / #1b1712;
--p4: #22303f / #f1e3c8; --p5: #ede3d0 / #a92f1c; --p6: #141619 / #f0a33a;
--p7: #5f6b33 / #f3efdc; --p8: #3b1f2b / #f0d8c8; --p9: #d9774a / #1d140f;
--p10: #2e5b8a / #f2ece0; /* write these as two tokens each in real code */
--font: "Big Shoulders Display", system-ui, sans-serif;
/* ring */
--cw: 280px; --ch: 400px; --r: 470px; --step: 36deg; /* 360 / 10 */
--perspective: 1700px;
--scroll-per-card: 46vh;
--px-per-card: 220px; /* drag */
--flick: 1.2; /* cards per px/ms of release velocity */
--dim-floor: .32; /* brightness of a card facing sideways */
/* motion */
--ease: cubic-bezier(.2, .7, .2, 1);
--t-settle: 160ms; /* idle time before snapping */
--t-held: 1600ms;
}
Typography
| Role | Family | Size | Weight | Line-height | Tracking | Case |
|---|---|---|---|---|---|---|
| Front title | Big Shoulders Display | 76px (54px < 820) | 800 | 0.9 | −0.01em | UPPERCASE, balanced wrap |
| Poster title | Big Shoulders Display | 52px (36px) | 900 | 0.86 | −0.01em | UPPERCASE |
| Poster meta | Big Shoulders Display | 13px (11px) | 700 | 1.4 | +0.14em | UPPERCASE |
| Brand | Big Shoulders Display | 22px | 900 | 1 | +0.06em | UPPERCASE |
| Nav | Big Shoulders Display | 16px | 700 | 1 | +0.08em | UPPERCASE |
| Kicker | Big Shoulders Display | 14px | 700 | 1.4 | +0.16em | UPPERCASE |
| Fact label | Big Shoulders Display | 14px | 500 | 1.4 | +0.1em | UPPERCASE |
| Fact value, body | Big Shoulders Display | 17–18px | 500 | 1.4 | 0 | sentence |
| CTA | Big Shoulders Display | 18px | 800 | 1 | +0.08em | UPPERCASE |
One condensed family, nine weights: the hierarchy is weight and size, not a second face.
Motion
| Element | Trigger | Property | From → To | Duration | Easing |
|---|---|---|---|---|---|
.ring | scroll position | rotateY | −f × 36° | scroll-linked | n/a |
.card | scroll position | filter: brightness, saturate | 0.32 + 0.68·max(0, cos(d·36°)); saturate 0.4 + 0.6·… | scroll-linked | n/a |
| page | idle 160ms, drag release, key, step, click | scroll position | to nearest / target card | browser smooth scroll | browser |
| Poster type | card is not the front card | visibility | visible → hidden | instant | n/a |
| Details row | front index changes | text | swap | instant | n/a |
| Book seats | click | label | “Book seats” → “Seats held” → back | 1600ms | n/a |
d is the signed card distance from the front, wrapped into −5…5.
Reduced motion: the ring only paints whole cards (round(f)), so it steps 36° at a time instead of sweeping, and every programmatic scroll is instant (behavior: 'auto'). Scroll, drag and keys still work; nothing glides.
States
- Front card: brightness 1, 1px
rgba(242,235,224,.4)ring, type visible. - Neighbour (±1): brightness ≈ 0.87, art only.
- Far (±2): brightness ≈ 0.53, art only. Cards facing away are hidden by
backface-visibility: hidden. - Dragging: cursor
grabbing; no snapping until release. - Step button hover: ring from
#6f685eto ink. CTA hover:#f7b55a. - Focus-visible: 2px amber outline, 3px offset; the stage itself shows a 2px inset amber ring.
- Ends: at card 1 Left does nothing; at card 10 Right does nothing.
Accessibility
- The stage is a focusable
regionwitharia-roledescription="carousel"and a label that names the keys. Posters arearia-hiddenbecause the details row holds the same information as real text, read in a sensible order. - Visually hidden text (the
<h1>and the live region) uses 1px,clip-path: inset(50%)andopacity: 0; it stays in the accessibility tree. - The live region announces “Film N of 10: Title, Director” when the front card changes.
- Step buttons have
aria-label“Previous film” / “Next film”; the Book link’saria-labelnames the film. touch-action: pan-ylets phones scroll vertically through the pin while horizontal drags turn the ring.- Contrast: ink on page 16.5:1; dim 7.3:1; CTA text 8.9:1; every poster ink on its ground is at least 4.8:1 (lowest:
#f4e6cfon#b23a24; olive#f3efdcon#5f6b33is 5.0:1). Step rings#6f685eare 3.5:1. - Hit targets: step buttons 44px, CTA 48px tall (44px on phones).
Responsive rules
- ≥ 1280: as specified.
- 1024–1279: same ring; the details grid columns shrink and the title wraps to two balanced lines inside the fixed 200px row.
- 820–1023: same; perspective unchanged.
- < 820: cards 190×272, radius 310px, perspective 1100px. Top bar keeps only the brand. Details stack: kicker, 54px title, facts, then a row with the step buttons and counter on the left and Book seats on the right. Side cards are cut by the screen edge and show art only.
- Nothing scrolls sideways at 390px; the stage clips the ring.
Acceptance checklist
Always
- Scroll position is the single source of truth; drag, keys, buttons and clicks all call
scrollTo. - Ring step is
360 / ndegrees and radius at least(w / 2) / tan(180° / n)plus a gap. - Card lighting follows
cosof the angular distance, floored at 0.32 brightness. - The ring snaps to a whole card 160ms after scrolling stops, only while pinned.
- Drag maps a fixed px per card and a flick adds cards from release velocity; a drag never triggers a click.
- Left, Right, Home and End work when the stage has focus; ends clamp.
- The details row and a polite live region update when the front card changes.
- Poster text is rendered only on the front card (dimmed cards carry no text).
- Reduced motion steps whole cards with instant scrolling.
- Nothing scrolls sideways at 390px.
This demo
- Ten films from “The Salt Year” (Ilse Varga, Hungary, 104 min) to “Last Bus to Ostra” (Jonas Kiel, Denmark, 85 min).
- Cards 280×400, radius 470px, perspective 1700px, 46vh of scroll per card.
- Amber
#f2a33aused only for the brand half, current nav item, kicker, CTA and focus.
Implementation notes
Build the ring once; rotate one element.
.ring { position: absolute; left: 50%; top: 50%; width: var(--cw); height: var(--ch);
margin: calc(var(--ch) / -2) 0 0 calc(var(--cw) / -2); transform-style: preserve-3d;
transform: translateZ(calc(var(--r) * -1)) rotateY(var(--a)); }
.card { position: absolute; inset: 0; backface-visibility: hidden;
transform: rotateY(calc(var(--i) * 36deg)) translateZ(var(--r));
filter: brightness(calc(.32 + var(--f, 0) * .68)) saturate(calc(.4 + var(--f, 0) * .6)); }
The translateZ(-r) on the ring puts the front card back at z = 0, so it renders at exactly 280×400.
One paint from scroll:
const fNow = () => clamp(0, (scrollY - top0()) / span() * (N - 1), N - 1);
const paint = (f) => {
const v = reduce ? Math.round(f) : f;
ring.style.setProperty('--a', -v * 36 + 'deg');
cards.forEach((c, k) => { let d = k - v; d -= N * Math.round(d / N);
c.style.setProperty('--f', Math.max(0, Math.cos(d * Math.PI / 5))); });
show(Math.round(f)); // details row, counter, live region
};
Drag writes scroll, release snaps with momentum:
const f = clamp(0, sf - (e.clientX - sx) / 220, N - 1);
scrollTo({ top: top0() + span() * f / (N - 1), behavior: 'auto' });
// on pointerup
go(Math.round(fNow() - vx * 1.2)); // vx in px/ms, sign flips because dragging left moves forward
Common mistakes: keeping a separate “current index” that drag and scroll update independently (they drift); snapping while the user is still scrolling (wait 160ms of quiet); dimming with opacity (it shows the backs of other cards through the front one, and the far cards’ text fails contrast); and forgetting touch-action: pan-y, which makes phones either refuse to scroll or refuse to drag.
Details
- Palette
- Type
- Big Shoulders Display
- Motion
- Rich motion
- Build
- Multi-step
- Tags
Studied
Haunted Boulder CityPin the screen and let scroll turn the pages. Only the line you are on gets to shout.
All sources