Design Lounge
Install

Nº 454 of 520

Three passes, one frame

A method section: one portrait frame shown as sketch, light and motion passes, with timed tabs and a drag line that compares each pass with the one before.

Preview

Open full size
1280 × 800 · 20.1 KB · 340 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 "Three passes, one frame" (piece stage-passes-compare) 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

207 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 gmxdigital.com: the idea is the “from an image to an experience” block, where one frame steps through base image, atmosphere and film while three numbered tabs underneath keep time. This version is the method section of Siltworks, a fictional visualisation studio, on a warm paper ground instead of a dark one. The same 400×520 frame of a lake pavilion is shown in three passes: 01 Sketch (line drawing on a grid), 02 Light (flat colour, sun, shadows) and 03 Motion (dusk grade, drifting clouds, birds, water shimmer, a lamp glow and a ticking REC timecode). The detail worth copying is the compare line: from pass 02 on, the previous pass sits under the current one, and a draggable divider shows how much the new pass changed. The page opens on pass 02 with the line at 50%, so the point is visible in the first frame.

Reference behaviour

  1. Load: pass 02 “Light” is selected. Tab 01 shows a full grey line (done). Tab 02’s 2px green line starts filling over 6000ms.
  2. The frame shows pass 01 on the left of the divider and pass 02 on the right. The divider wipes in from 100% to 50% over 700ms (ease-out quart). Chips: “Pass 01” (paper chip) top-left, “Pass 02 · Light” (ink chip) top-right.
  3. Left column shows “02 / 03”, the italic step title “Light and weather”, a 3-line description, and a link “Read the light study ↗”. The step block fades up 8px over 420ms on each change.
  4. When tab 02’s line completes, pass 03 becomes selected and the wipe runs again with pass 02 now underneath. After 03 it wraps to 01.
  5. Pass 01 has no previous pass: no divider, no knob, no left chip. The frame shows the sketch alone.
  6. Pass 03 adds the moving layer: clouds drift ±30px (26s and 34s alternate), two birds cross the frame every 9s, five water lines shimmer, the window glow breathes between 55% and 100%, the scene pushes in to scale 1.04 over 6s, and “REC 00:SS:FF” counts at 24 frames per second with a blinking red dot.
  7. Drag the 44px knob (or click anywhere in the frame) to move the divider. Dragging also turns autoplay off (Pause becomes Play).
  8. Hovering the frame pauses the tab timer. Leaving resumes it unless Pause is on.
  9. Pause button (40px pill, right of the tabs) toggles autoplay. The current tab’s line freezes in place.
  10. Keyboard: tabs are a tablist. Left / Right arrows move and select, Home / End jump. The knob is a slider: Left / Right ±5, PageUp / PageDown ±20, Home 0, End 100.
  11. A polite live region announces “Pass 3 of 3: The moving frame”.
  12. Reduced motion: autoplay starts paused, the wipe jumps straight to 50%, no clouds, birds, shimmer, push or blinking; the timecode still updates as text.

Structure

1280 × 800, padding-inline 56px, paper ground --bg
┌──────────────────────────────────────────────────────────────────────────┐
│ —— 03 / Method      Sketch, light and motion, in one frame.  Skip to ↘  │ head 84px, 1px rule
├──────────────────────────────────────────┬───────────────────────────────┤
│ From a sketch                            │      ┌───────────────────┐    │
│ to a moving frame. (serif italic green)  │      │Pass 01   Pass 02 ·│    │ frame 400×520
│ intro 16px, 430px                        │      │  sketch  │  lit   │    │ centred in column
│ ──────────────────────────────           │      │          ◉        │    │ knob 44px
│ 02 / 03                                  │      │          │        │    │
│ Light and weather (34px serif italic)    │      │          │  REC ● │    │
│ description, 440px                       │      └───────────────────┘    │
│ Read the light study ↗                   │                               │
├──────────────────────────────────────────┴───────────────────────────────┤
│ 01 Sketch ─────────   02 Light ━━━━──────   03 Motion ─────────  (‖ Pause)│ tabs row, 2px lines
└──────────────────────────────────────────────────────────────────────────┘
body grid: 520px | 1fr, gap 56px
  • main.wrap: grid rows head / body / tabs.
  • Head: three-column grid. Label span, middle span, skip link.
  • Body left: section labelled by the h2. The step block is the role="tabpanel", tabindex="0", aria-labelledby the selected tab.
  • Body right: figure.frame with two absolute layers (#under previous pass, #over current pass, clipped), two chips, a divider holding a button role="slider", and the REC label.
  • Tabs row: a role="tablist" of three buttons (number + name + absolute 2px bar), then the pause button.
  • The scene is one SVG template, cloned into both layers. The pass is a class on the SVG root: sk, lt, mo.

Tokens

:root {
  --bg: #ece7dc;        /* paper ground */
  --paper: #f6f2ea;     /* frame and chips */
  --ink: #1d211c;       /* headings, sketch lines */
  --ink-2: #474d45;     /* body text */
  --ink-3: #6a7067;     /* counters, idle tabs */
  --line: #d3ccbe;      /* rules, idle tab lines, sketch grid */
  --line-2: #b9b1a1;    /* frame edge, done tab lines, button borders */
  --accent: #2f6b4f;    /* forest green: italic word, active tab, focus */
  --accent-2: #e3b45a;  /* lamp and sun, used only inside the scene */

  --sans: "Hanken Grotesk", system-ui, sans-serif;
  --serif: "Literata", Georgia, serif;  /* italic only */

  --space: 4px 8px 12px 16px 24px 28px 56px;
  --frame-w: 400px; --frame-h: 520px;
  --shadow-frame: 0 0 0 1px var(--line-2), 0 30px 60px -30px rgba(29,33,28,.35);

  --ease: cubic-bezier(.2,.7,.2,1);
  --expo: cubic-bezier(.16,1,.3,1);
  --dwell: 6000ms;      /* per pass */
  --wipe: 700ms;        /* divider 100% → 50% */
}

Typography

RoleFamilySizeWeightLine-heightTrackingNotes
Section headingHanken Grotesk60px5001.02-0.035emLast words in Literata italic 400, green
Step titleLiterata italic34px4001.1-0.015em--ink
BodyHanken Grotesk16px4001.550--ink-2, max 440px
Counter “02 / 03”Hanken Grotesk12px60010.12em--ink-3
Head rowHanken Grotesk13px500–60010Label green with 28px rule
Tab numberHanken Grotesk26px4001-0.02emGreen when selected
Tab nameHanken Grotesk14px60010
Chips, RECHanken Grotesk12px60010.02–0.08emREC uses tabular numerals

Serif italic is used for exactly two things: the end of the heading and the step title. Nothing else.

Motion

ThingTriggerPropertyFrom → toDurationEasingReduced motion
Divider wipepass change (02, 03)--x via rAF100% → 50%700msease-out quartjump to 50%
Step blockpass changeopacity, translateY0, 8px → 1, 0420ms--easenone
Tab linetab selectedscaleX0 → 16000mslinear (timer)full, static
Scene pushpass 03scale1 → 1.046s, oncecubic-bezier(.25,.6,.3,1)none
Cloudspass 03translateX-24px ↔ 30px26s / 34s alternatelinearoff
Birdspass 03translate(-60, 10) → (460, -30)9s looplinearoff
Water shimmerpass 03opacity, translateX.15, -4px ↔ .7, 4px2.8s alternate, half offsetease-in-outoff
Lamp glowpass 03opacity.55 ↔ 14s alternateease-in-outoff
REC dotpass 03opacity1 / .21.2s stepssteps(1)steady
Knobhover / pressscale1.06 / .96160ms--easenone
Sketch fillspass class changefill—400ms--easenone

States

  • Tab idle: --ink-3, 2px --line underline.
  • Tab hover: --ink-2.
  • Tab selected: --ink, number green, green bar filling. aria-selected="true", tabindex=0.
  • Tab done (before the selected one): bar full in --line-2.
  • Frame hovered: timer paused.
  • Divider hidden on pass 01 (.has-prev removed). Left chip hidden too.
  • Knob focus-visible: 2px green outline, offset 3px. The ring sits outside the 44px circle.
  • Pause on: play icon, text “Play”, aria-pressed="true".
  • Link hover: underline darkens from --line-2 to --ink. “Skip to projects” arrow moves 2px down-right.

Accessibility

  • Tablist “Passes” with three tabs. Roving tabindex. The step block is the tabpanel.
  • The knob is role="slider" with aria-valuemin=0, aria-valuemax=100, aria-valuenow, and aria-valuetext="50 percent previous pass".
  • The figure has aria-label="Pavilion at Lake Orta, shown at the current pass". The scene SVGs are aria-hidden; the step text describes each pass.
  • The live region names the pass on change. The REC timecode is not announced.
  • The pause control is in the tab order right after the tabs. Autoplay stops for good once the reader drags the divider.
  • Contrast: #1d211c on #ece7dc is about 14:1; #474d45 about 7.6:1; green #2f6b4f about 5.3:1; #6a7067 counters about 4.6:1.
  • Targets: knob 44px, tabs 48px tall, pause 40px tall.

Responsive rules

  • ≥1280: as drawn.
  • 1024: body grid becomes 1fr 400px, gap 32px, heading 48px.
  • 768: same as 1024; if the left column gets tight, the description wraps to 4 lines. The step block keeps min-height: 228px so the tabs never jump.
  • <760: padding 20px, head drops its middle text, body stacks (text then frame). Frame is width: min(100%, 400px) with aspect-ratio: 400/520. Tabs become three columns with the number above the name; Pause moves to its own row, left aligned.
  • No horizontal overflow at 375px.

Acceptance checklist

Always

  • One frame, one camera. Every pass is the same composition; only the rendering changes.
  • Three tabs with a timer line each. Completed tabs show a full neutral line.
  • From the second pass on, the previous pass sits under the current one, split by a draggable divider. The first pass has no divider.
  • The divider wipes from 100% to 50% on every pass change.
  • Dragging, clicking in the frame, or using the slider keys moves the divider and turns autoplay off.
  • Hovering the frame pauses the timer. A visible Pause / Play button exists.
  • The current pass chip is top-right; the previous pass chip is top-left, on the side where that pass shows.
  • Tablist and slider keyboard support as listed.
  • Reduced motion: paused autoplay, no loops, divider at 50% with no wipe.
  • No horizontal overflow at 375px.

This demo

  • Opens on “02 / 03 Light and weather” with the divider at 50%.
  • Passes are 01 Sketch, 02 Light, 03 Motion; titles “Massing and line”, “Light and weather”, “The moving frame”.
  • Heading “From a sketch to a moving frame.” with “moving frame.” in Literata italic #2f6b4f.
  • Pass 03 shows “REC 00:SS:FF” counting at 24fps with a red dot.
  • Frame is 400×520 on #f6f2ea with a 1px #b9b1a1 edge.

Implementation notes

  1. One scene, three looks. Draw the scene once with colours as SVG fill attributes, give every surface a class f, and let a class on the root restyle it. CSS beats presentation attributes, so the sketch pass is just an override:
.sk .f { fill: var(--paper); stroke: var(--ink); stroke-width: 1.1; }
.sk .shade, .sk .anim, .sk .grade, .sk .glow { display: none; }
.sk .cons, .sk .grid { display: block; }   /* construction lines, pencil grid */
.lt .anim, .lt .grade, .lt .glow { display: none; }
.mo .grade { display: block; }              /* dusk gradient over everything */
  1. The compare. Both layers are position:absolute; inset:0. Only the top layer is clipped, with a custom property the divider also reads. A common mistake is leaving the layer wrappers unpositioned: a zero-height box with a clip-path hides its whole content.
#under, #over { position: absolute; inset: 0; }
.over { clip-path: inset(0 0 0 var(--x, 0%)); }
.divider { position: absolute; top: 0; bottom: 0; left: var(--x, 0%); }
const setX = v => { x = Math.max(0, Math.min(100, v));
  frame.style.setProperty('--x', x + '%');
  knob.setAttribute('aria-valuenow', Math.round(x)); };
knob.addEventListener('pointerdown', e => { knob.setPointerCapture(e.pointerId); stopAutoplay(); });
knob.addEventListener('pointermove', e => { if (e.buttons) {
  const r = frame.getBoundingClientRect(); setX((e.clientX - r.left) / r.width * 100); } });
  1. Cloned SVGs need unique gradient ids. When you inject the template twice, rename every id and url(#…) per copy (the demo appends a counter), or the second copy silently paints with the first copy’s gradients.

Common mistakes:

  • Letting the motion pass differ only by tiny animation. Add the dusk grade and the lamp glow so pass 03 reads as a different time of day even in a still.
  • Running the wipe with CSS transitions on clip-path while also dragging. Drive one custom property from one place.
  • Opening on pass 01, which has nothing to compare.
  • Putting labels on the wrong side of the divider.

Details

Palette
Type
Hanken Grotesk · Literata
Motion
Subtle motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Stripe

Let one big moving thing carry the colour. Keep the words light and the cards real.

All sources