Design Lounge
Install

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

Open full size
1280 × 800 · 16.7 KB · 171 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 "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.

Brief

203 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

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
  • .wrap — CSS grid. .copy left; .aside right.
  • 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"> — .vh header, .plot (sun + 6 .bed + .ruler), .foot with 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

RoleFamilySizeWeightLine-heightTrackingCase
BodyFigtree16px4001.60sentence
Eyebrow / step kickerFigtree12px6001+0.16em / +0.14emUPPERCASE
HeadlineYoung Serif76px4000.98−0.03emsentence
Lede / step pFigtree17px4001.60sentence
Step titleYoung Serif36px4001.08−0.02emsentence
NumeralYoung Serif15px40040px circle0numerals
Stat valueYoung Serif26px4001.10mixed
Stat labelFigtree14px4001.60sentence
Card titleYoung Serif20px40010sentence
Card metaFigtree13px40010sentence
Crop nameYoung Serif17px4001.10sentence
Bed dim / kg unitFigtree11px / 12px400/50010mixed
kg figureYoung Serif34px40010numerals
TallyYoung Serif92px4000.9−0.04emnumerals
CalendarFigtree11px400/500/60010mixed
NoteFigtree13px4001.60sentence

Motion

ElementTriggerPropertyFrom → ToDurationEasingNotes
.step opacityIO / clickopacity0.38 → 1560ms--easeinactive stay 0.38, not 0
Numeral fillsamebackground, color, borderpaper/line → ink/bg180ms--ease
Bed fill / borderleave step 1background, border-colordashed empty → solid --c560ms--ease
Crop + rowsleave step 1opacity, translateY0, 6px → 1, 0560msease / out
Tick discsstep 3opacity, scale0, .5 → 1, 1180 / 560— / outbeds 4–6 stay hidden
kg figuresstep 4opacity, scale0, .8 → 1, 1560msease / outorigin right top
Spark barsstep 4transform scaleY0 → 1560ms--ease-outdelay i * 30ms
Footer panelsdata-stepopacity, translateY0, 12px → 1, 0560msease / outonly matching .pN
Beds 4–6step 3scale1 → 0.97560ms--ease-out
htmlanchorsscroll-behaviorsmooth——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 --ink fill and --bg type, aria-current="step" on the button. Others aria-current="false".
  • Step 1 exclusive: sun opacity 1; ruler visible; bed 2 (nth-child(2)) 1.5px solid --terra and rgba(198,93,59,.06) fill; its .dim terracotta 600; two 9px square handles (::before top-right, ::after bottom-right).
  • Steps 2–4: .dim opacity 0; .crop and .rows visible; 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; .kg visible.
  • 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> with aria-label="Go to step N".
  • Figure has aria-label="Plot planner preview". Calendar “Today · 2 Jun” and sparkline are aria-hidden where decorative.
  • Keyboard: Tab through 1–4; Enter/Space on a numeral scrolls that step to centre. No arrow-key requirement.
  • Contrast: --ink-2 on --bg is ~5.8:1; --ink-3 is used only for 11–12px meta. Numeral in the on-state is --bg on --ink.
  • Hit target: 40×40px numerals. Step blocks themselves are not buttons.
  • If IntersectionObserver is missing, clicking numerals still calls set.

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: 16px and 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: smooth off under reduced motion.

Acceptance checklist

  • Layout is 1fr / 600px with 72px gap at 1280; the right card is sticky at top: 48px and 704px tall.
  • Exactly four steps; inactive opacity is 0.38; last step min-height 86vh.
  • IntersectionObserver uses rootMargin: '-45% 0px -50% 0px' and writes data-step plus aria-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
Designed bySusan AcharyaKathmandu

Studied

Linear

Show the product doing the work. Fade it at the edges and keep one colour for what matters.

All sources