Nº 150 of 520
Sticky scroll feature steps
Four Loam garden-planning steps scroll on the left while a 600px plot preview sticks on the right and morphs beds, crops, calendar and harvest tally.
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 scroll feature steps" (piece features-sticky-scroll-steps) 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º 150 · "Sticky scroll feature steps" · www.designlounge.live -->
# Sticky scroll feature steps
> **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 features section for a kitchen-garden planner ("Loam"). Four long steps live in the left column; a 600px garden-plot card sticks in the right column and restyles itself as each step crosses the middle of the viewport. Step one is empty dashed beds and a sun disc; two fills them with companion crops; three stamps checkmarks and a Mar–Oct calendar; four drops kilogram tallies and a 12-bar sparkline. The feeling is paper, soil and serif: warm `#F4EFE6`, terracotta `#C65D3B` on one word of the 76px headline. The detail worth copying is that the sticky card is one DOM figure whose `data-step` drives every visual — no four stacked mockups, no image swaps.
## Reference behaviour
1. Initial state: step 1 is `.on` (opacity 1, numeral filled `--ink`). Steps 2–4 sit at opacity 0.38. The figure `data-step="1"`: six dashed empty beds, a 6.20 m ruler, a butter sun in the top-left, bed 2 (courgette slot) highlighted with a terracotta border and two 9px corner handles. Footer legend: 6 beds / 7 h 40 m sun / loam pH 6.8 / last frost 12 May.
2. Scrolling until a step's block intersects a band from 45% from the top to 50% from the bottom (`rootMargin: -45% 0px -50% 0px`) calls `set(n)`. The matching numeral gets `aria-current="step"`.
3. Clicking a numeral `scrollIntoView({ block: 'center' })` and sets that step immediately.
4. **Step 2:** dashed borders become solid fills using each bed's `--c`; crop names and row textures fade in; sun and ruler hide; legend becomes crop families (legumes, cucurbits, nightshades, roots).
5. **Step 3:** 22px ink check discs appear on the first three beds; beds 4–6 scale to 0.97 and hide their ticks. Footer is a 12-column calendar (Mar–Oct) with Sow / Thin / Feed / Harvest bars and a "Today · 2 Jun" hairline at 41% of the plot width.
6. **Step 4:** kilogram figures (9.2, 11.6, 7.8, 4.1, 3.9, 1.8) scale in at the top-right of each bed; row texture drops to 0.35 opacity; footer shows **38.4 kg** and a 12-bar sparkline that grows from `scaleY(0)` with 30ms stagger. Bars 9–12 are terracotta; 1–8 sage.
7. Morphs use 560ms `--ease` on colour/opacity and `--ease-out` on transform. Last step is 86vh tall so it can reach the observer band; others 62vh.
8. Reduced motion: `scroll-behavior: auto`; all transitions 1ms. IntersectionObserver still updates `data-step`.
## Structure
```
1280 × 800, .wrap grid 1fr 600px, gap 72px, pad 0 72px, max-width 1440
┌────────────────────────────────┬──────────────────────────────────────┐
│ pad-top 64 │ .aside → .sticky top 48, h 704 │
│ — HOW LOAM WORKS │ ┌ viz card, r 18, pad 22 24 ────────┐│
│ From bare soil to a │ │ Back garden Plot · 6.2×4.0 m ││
│ full basket. (76px, em terra) │ │ faces SSW ││
│ lede 17px, max 430 │ │ ┌ plot 336h, 3×2 beds, 34×18 gap ┐││
│ │ │ │ sun │││
│ ① STEP ONE · MAP │ │ │ [beans] [courgette] [tomatoes] │││
│ Draw your plot… (62vh) │ │ │ [kale] [carrots] [beetroot] │││
│ 6.2 × 4.0 m | 7 h 40 m │ │ │ ruler 6.20 m │││
│ ② STEP TWO · CHOOSE (62vh) │ │ └─────────────────────────────────┘││
│ ③ STEP THREE · TEND (62vh) │ │ foot 176h — one .panel visible ││
│ ④ STEP FOUR · HARVEST (86vh) │ └────────────────────────────────────┘│
└────────────────────────────────┴──────────────────────────────────────┘
timeline: 1px --line-2 at x=19; 40px numeral at left 0, top 40
… 163 more lines. Copy or download for the full brief. Loading the source…Brief
203 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 features section for a kitchen-garden planner (“Loam”). Four long steps live in the left column; a 600px garden-plot card sticks in the right column and restyles itself as each step crosses the middle of the viewport. Step one is empty dashed beds and a sun disc; two fills them with companion crops; three stamps checkmarks and a Mar–Oct calendar; four drops kilogram tallies and a 12-bar sparkline. The feeling is paper, soil and serif: warm #F4EFE6, terracotta #C65D3B on one word of the 76px headline. The detail worth copying is that the sticky card is one DOM figure whose data-step drives every visual — no four stacked mockups, no image swaps.
Reference behaviour
- Initial state: step 1 is
.on(opacity 1, numeral filled--ink). Steps 2–4 sit at opacity 0.38. The figuredata-step="1": six dashed empty beds, a 6.20 m ruler, a butter sun in the top-left, bed 2 (courgette slot) highlighted with a terracotta border and two 9px corner handles. Footer legend: 6 beds / 7 h 40 m sun / loam pH 6.8 / last frost 12 May. - Scrolling until a step’s block intersects a band from 45% from the top to 50% from the bottom (
rootMargin: -45% 0px -50% 0px) callsset(n). The matching numeral getsaria-current="step". - Clicking a numeral
scrollIntoView({ block: 'center' })and sets that step immediately. - Step 2: dashed borders become solid fills using each bed’s
--c; crop names and row textures fade in; sun and ruler hide; legend becomes crop families (legumes, cucurbits, nightshades, roots). - Step 3: 22px ink check discs appear on the first three beds; beds 4–6 scale to 0.97 and hide their ticks. Footer is a 12-column calendar (Mar–Oct) with Sow / Thin / Feed / Harvest bars and a “Today · 2 Jun” hairline at 41% of the plot width.
- Step 4: kilogram figures (9.2, 11.6, 7.8, 4.1, 3.9, 1.8) scale in at the top-right of each bed; row texture drops to 0.35 opacity; footer shows 38.4 kg and a 12-bar sparkline that grows from
scaleY(0)with 30ms stagger. Bars 9–12 are terracotta; 1–8 sage. - Morphs use 560ms
--easeon colour/opacity and--ease-outon transform. Last step is 86vh tall so it can reach the observer band; others 62vh. - Reduced motion:
scroll-behavior: auto; all transitions 1ms. IntersectionObserver still updatesdata-step.
Structure
1280 × 800, .wrap grid 1fr 600px, gap 72px, pad 0 72px, max-width 1440
┌────────────────────────────────┬──────────────────────────────────────┐
│ pad-top 64 │ .aside → .sticky top 48, h 704 │
│ — HOW LOAM WORKS │ ┌ viz card, r 18, pad 22 24 ────────┐│
│ From bare soil to a │ │ Back garden Plot · 6.2×4.0 m ││
│ full basket. (76px, em terra) │ │ faces SSW ││
│ lede 17px, max 430 │ │ ┌ plot 336h, 3×2 beds, 34×18 gap ┐││
│ │ │ │ sun │││
│ ① STEP ONE · MAP │ │ │ [beans] [courgette] [tomatoes] │││
│ Draw your plot… (62vh) │ │ │ [kale] [carrots] [beetroot] │││
│ 6.2 × 4.0 m | 7 h 40 m │ │ │ ruler 6.20 m │││
│ ② STEP TWO · CHOOSE (62vh) │ │ └─────────────────────────────────┘││
│ ③ STEP THREE · TEND (62vh) │ │ foot 176h — one .panel visible ││
│ ④ STEP FOUR · HARVEST (86vh) │ └────────────────────────────────────┘│
└────────────────────────────────┴──────────────────────────────────────┘
timeline: 1px --line-2 at x=19; 40px numeral at left 0, top 40
.wrap— CSS grid..copyleft;.asideright.- Eyebrow: 12/600 +0.16em uppercase terracotta, 24×1px rule
::before. <h2>76px Young Serif;<em>full basket.</em>terracotta,font-style:normal.<ol class="steps">— four<li class="step" data-s>. Each:<button class="num">,<small>,<h3>,<p>, two-stat<ul>..sticky—position:sticky; top:48px; height:704px; padding-top:16px.<figure class="viz" data-step aria-label="Plot planner preview">—.vhheader,.plot(sun + 6.bed+.ruler),.footwith four.panel.p1–p4.- Beds in DOM order: runner beans
#c9d3b9, courgette#efd79c, tomatoes#efc3b2, kale#d6dcc8, carrots#e6d3b6, beetroot#dcc9d6. Each has.rows,.dim,.crop,.tick(check SVG),.kg.
Tokens
:root {
/* colour — warm paper, soil ink, terracotta accent, garden fills */
--bg: #f4efe6;
--card: #fbf8f2;
--ink: #23291f;
--ink-2: #5b6152;
--ink-3: #8d8f80;
--line: #ddd5c6;
--line-2: #cbc1ae;
--terra: #c65d3b;
--sage: #a9b89a;
--sage-d: #6f8460;
--butter: #ebcb7a;
--rose: #e9b8a6;
--soil: #d9c6a8;
/* type */
--serif: "Young Serif", Georgia, serif;
--sans: "Figtree", system-ui, sans-serif;
/* layout */
--aside-w: 600px;
--sticky-h: 704px;
--plot-h: 336px;
--r: 18px;
--r-bed: 10px;
--shadow: 0 1px 0 rgba(35, 41, 31, .04), 0 30px 60px -30px rgba(70, 50, 20, .28);
/* motion */
--t-fast: 180ms;
--t-morph: 560ms;
--ease: cubic-bezier(.2, .7, .2, 1);
--ease-out: cubic-bezier(.16, 1, .3, 1);
}
Typography
| Role | Family | Size | Weight | Line-height | Tracking | Case |
|---|---|---|---|---|---|---|
| Body | Figtree | 16px | 400 | 1.6 | 0 | sentence |
| Eyebrow / step kicker | Figtree | 12px | 600 | 1 | +0.16em / +0.14em | UPPERCASE |
| Headline | Young Serif | 76px | 400 | 0.98 | −0.03em | sentence |
| Lede / step p | Figtree | 17px | 400 | 1.6 | 0 | sentence |
| Step title | Young Serif | 36px | 400 | 1.08 | −0.02em | sentence |
| Numeral | Young Serif | 15px | 400 | 40px circle | 0 | numerals |
| Stat value | Young Serif | 26px | 400 | 1.1 | 0 | mixed |
| Stat label | Figtree | 14px | 400 | 1.6 | 0 | sentence |
| Card title | Young Serif | 20px | 400 | 1 | 0 | sentence |
| Card meta | Figtree | 13px | 400 | 1 | 0 | sentence |
| Crop name | Young Serif | 17px | 400 | 1.1 | 0 | sentence |
| Bed dim / kg unit | Figtree | 11px / 12px | 400/500 | 1 | 0 | mixed |
| kg figure | Young Serif | 34px | 400 | 1 | 0 | numerals |
| Tally | Young Serif | 92px | 400 | 0.9 | −0.04em | numerals |
| Calendar | Figtree | 11px | 400/500/600 | 1 | 0 | mixed |
| Note | Figtree | 13px | 400 | 1.6 | 0 | sentence |
Motion
| Element | Trigger | Property | From → To | Duration | Easing | Notes |
|---|---|---|---|---|---|---|
.step opacity | IO / click | opacity | 0.38 → 1 | 560ms | --ease | inactive stay 0.38, not 0 |
| Numeral fill | same | background, color, border | paper/line → ink/bg | 180ms | --ease | |
| Bed fill / border | leave step 1 | background, border-color | dashed empty → solid --c | 560ms | --ease | |
| Crop + rows | leave step 1 | opacity, translateY | 0, 6px → 1, 0 | 560ms | ease / out | |
| Tick discs | step 3 | opacity, scale | 0, .5 → 1, 1 | 180 / 560 | — / out | beds 4–6 stay hidden |
| kg figures | step 4 | opacity, scale | 0, .8 → 1, 1 | 560ms | ease / out | origin right top |
| Spark bars | step 4 | transform scaleY | 0 → 1 | 560ms | --ease-out | delay i * 30ms |
| Footer panels | data-step | opacity, translateY | 0, 12px → 1, 0 | 560ms | ease / out | only matching .pN |
| Beds 4–6 | step 3 | scale | 1 → 0.97 | 560ms | --ease-out | |
| html | anchors | scroll-behavior | smooth | — | — | auto if reduced |
Do not animate data-step itself. CSS selectors: .viz[data-step="1"] .p1 (etc.) set the visible panel; .viz:not([data-step="1"]) fills beds.
States
- Active step:
.step.on, opacity 1, numeral--inkfill and--bgtype,aria-current="step"on the button. Othersaria-current="false". - Step 1 exclusive: sun opacity 1; ruler visible; bed 2 (
nth-child(2)) 1.5px solid--terraandrgba(198,93,59,.06)fill; its.dimterracotta 600; two 9px square handles (::beforetop-right,::afterbottom-right). - Steps 2–4:
.dimopacity 0;.cropand.rowsvisible; bed border solid transparent (colour comes from background--c). - Step 3 ticks: first three beds only (
.bed:nth-child(n+4) .tick { opacity: 0 }). - Step 4 rows: opacity 0.35;
.kgvisible. - Numeral focus-visible: 2px terracotta outline, 3px offset.
- Spark:
.spark i:nth-child(n+9)terracotta; heights 12, 18, 26, 40, 52, 70, 88, 100, 84, 61, 37, 20%.
Accessibility
- Steps are an
<ol>. Each numeral is a real<button>witharia-label="Go to step N". - Figure has
aria-label="Plot planner preview". Calendar “Today · 2 Jun” and sparkline arearia-hiddenwhere decorative. - Keyboard: Tab through 1–4; Enter/Space on a numeral scrolls that step to centre. No arrow-key requirement.
- Contrast:
--ink-2on--bgis ~5.8:1;--ink-3is used only for 11–12px meta. Numeral in the on-state is--bgon--ink. - Hit target: 40×40px numerals. Step blocks themselves are not buttons.
- If
IntersectionObserveris missing, clicking numerals still callsset.
Responsive rules
- ≥ 1280: as specified.
- ≤ 1100: grid
1fr 480px, gap 40px, padding 0 40px, headline 60px, plot height 300px. - 768–1023: stack — copy first, sticky card becomes static under the intro (or sticky with
top: 16pxand height auto). Steps keep 62vh so IO still works. - < 640: single column, padding 20px, headline 48px, hide the 6.20 m ruler text if it clips. Spark bars stay 12px wide.
scroll-behavior: smoothoff under reduced motion.
Acceptance checklist
- Layout is
1fr / 600pxwith 72px gap at 1280; the right card is sticky attop: 48pxand 704px tall. - Exactly four steps; inactive opacity is 0.38; last step min-height 86vh.
- IntersectionObserver uses
rootMargin: '-45% 0px -50% 0px'and writesdata-stepplusaria-current="step". - Clicking a numeral centres that step and updates the card without waiting for the observer.
- Step 1 shows dashed beds, sun, ruler “6.20 m”, and a terracotta selection on the courgette bed.
- Step 2 fills beds with the six crop colours and names listed in Structure.
- Step 3 shows checks on the first three beds only and the Mar–Oct calendar with “Today · 2 Jun”.
- Step 4 shows kg tallies, 38.4 kg, and 12 spark bars that grow with 30ms stagger.
- Morph duration is 560ms; reduced motion is 1ms and still changes step.
- Headline is 76px Young Serif; “full basket.” is terracotta.
- Copy matches Loam: 6.2 × 4.0 m, 7 h 40 m, 14 crops, 12 min/week, 38.4 kg, £212.
- Numeral focus rings are 2px terracotta.
Implementation notes
One figure, four looks. Drive everything from data-step — do not mount four previews:
.viz[data-step="1"] .p1,
.viz[data-step="2"] .p2,
.viz[data-step="3"] .p3,
.viz[data-step="4"] .p4 { opacity: 1; transform: none; }
.viz:not([data-step="1"]) .bed { border-style: solid; background: var(--c); }
Observer band in the middle of the viewport, not “when the top hits the top”:
const io = new IntersectionObserver(
(es) => es.forEach((e) => { if (e.isIntersecting) set(e.target.dataset.s); }),
{ rootMargin: '-45% 0px -50% 0px' }
);
Spark stagger — set delay once, grow with a class on the figure:
spark.forEach((b, i) => { b.style.transitionDelay = (i * 30) + 'ms'; });
/* CSS */ .viz[data-step="4"] .spark i { transform: scaleY(1); }
Common mistakes: making the right column position:fixed (it will overlap the next section on a real page); using four stacked images instead of CSS beds; putting min-height: 100vh on every step so step 4 never reaches the observer (the last one must be taller); fading inactive steps to 0 (the timeline should stay readable); forgetting counter/data-s as strings when comparing in set.
Details
- Palette
- Type
- Young Serif · Figtree
- Motion
- Rich motion
- Build
- One session
- Tags
Studied
LinearShow the product doing the work. Fade it at the edges and keep one colour for what matters.
All sourcesOn the shelves