Nº 361 of 520
Polaroid frames, three ways
One 300px instant-photo frame in three treatments: a hard shadow lit by the cursor, tape with a peelable sticky note, and a print you shake to develop.
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 "Polaroid frames, three ways" (piece card-polaroid-frames) 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º 361 · "Polaroid frames, three ways" · www.designlounge.live -->
# Polaroid frames, three ways
> **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 contact sheet with one instant-photo frame shown three ways, side by side on a dotted off-white board. A: a black-bordered frame with a solid 10px hard shadow that always falls away from the cursor, as if the pointer were the lamp. B: a softer frame held up by a strip of tomato washi tape, with a yellow sticky note slapped on its corner that you click to peel up, showing a date written underneath. C: a print still developing. It starts milky and blurred at 35%, and you drag it side to side to shake it. As you shake, the haze lifts and the colour comes back, and at 100% the caption writes itself on in marker. Each frame is a 300px card with 16px borders and a 68px marker caption strip. The detail worth copying is that the three treatments share one frame component and differ only in a modifier class.
## Reference behaviour
1. First frame: three cells in a row, 300px each, 72px gaps, vertically centred under a header rule. Labels under each: "A Hard shadow / light follows you", "B Tape + note / click to peel", "C Developing / drag to shake". C shows a 35% meter and an "Eject new" button below its label.
2. A, pointer anywhere on the page (mouse only): the shadow direction is the vector from the pointer to the frame centre. Its length is `min(14, 6 + distance / 60)` px. Updates run once per animation frame; the box-shadow eases over 140ms.
3. A, keyboard: focus the frame, Arrow keys move the shadow 2px per press (Left moves it left, as if the light moved right), clamped to ±14px. Home resets it to 10px, 10px.
4. A, pressed (`:active`): the frame moves 60% of the shadow offset toward the shadow and the shadow shrinks to 40%, so it reads as pressed into the board.
5. B, hover the sticky note: it straightens from 7° to 4° and rises 3px.
6. B, click or Enter/Space on the note: it peels up to `rotate(-10deg) translate(8px, -150px)` with a springy 380ms ease, the shadow grows from 5px to 9px 12px, the small label changes "peel me" → "stick back", and the date "08 · 07 · 26" written on the frame is now visible. Click again to stick it back.
7. C, drag: the frame follows the hand by half the accumulated x travel (clamped ±40px, so ±20px of movement), tilts 0.12° per px, and every pixel of movement adds `(|dx| + |dy| × 0.5) / 3600` to the development. Release springs it back over 420ms.
8. C, keyboard: Space or Enter adds 0.10 and plays a 360ms side-to-side nudge.
9. C at 100%: the caption "Gran's tomatoes, again" reveals left to right over 900ms by `clip-path`. The meter fills black. "Eject new" resets development to 0.
10. Reduced motion: no transitions, no nudge keyframes, C does not move while shaken but still develops.
## Structure
```
1280 × 800, body padding 36px 48px 40px, dotted board (22px grid of 1px dots)
CONTACT SHEET 03 — POLAROID FRAMES Three treatments for one 300px frame. Move, peel, shake.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2px rule
┌───────────┐▌ ▒▒tape▒▒ ┌───────────┐
│ desert │▌ ┌───────────┐ │ milky │
│ road │▌ │ lake + │ │ tomato │
│ 268×268 │▌ │ red coat │ │ vines │
│ │▌ │ ┌────────┐ │ │
│Route 9, │▌ │Juno at│ sticky │ │ (caption │
└───────────┘▌ └───────│ 152px │ └───────────┘
▀▀▀▀▀▀▀▀▀▀▀▀ └────────┘
[A] Hard shadow [B] Tape + note [C] Developing
▬▬▬▬▬▬ 35% [EJECT NEW]
rotate −3° rotate 2.5° rotate −1.5°
… 190 more lines. Copy or download for the full brief. Loading the source…Brief
230 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 contact sheet with one instant-photo frame shown three ways, side by side on a dotted off-white board. A: a black-bordered frame with a solid 10px hard shadow that always falls away from the cursor, as if the pointer were the lamp. B: a softer frame held up by a strip of tomato washi tape, with a yellow sticky note slapped on its corner that you click to peel up, showing a date written underneath. C: a print still developing. It starts milky and blurred at 35%, and you drag it side to side to shake it. As you shake, the haze lifts and the colour comes back, and at 100% the caption writes itself on in marker. Each frame is a 300px card with 16px borders and a 68px marker caption strip. The detail worth copying is that the three treatments share one frame component and differ only in a modifier class.
Reference behaviour
- First frame: three cells in a row, 300px each, 72px gaps, vertically centred under a header rule. Labels under each: “A Hard shadow / light follows you”, “B Tape + note / click to peel”, “C Developing / drag to shake”. C shows a 35% meter and an “Eject new” button below its label.
- A, pointer anywhere on the page (mouse only): the shadow direction is the vector from the pointer to the frame centre. Its length is
min(14, 6 + distance / 60)px. Updates run once per animation frame; the box-shadow eases over 140ms. - A, keyboard: focus the frame, Arrow keys move the shadow 2px per press (Left moves it left, as if the light moved right), clamped to ±14px. Home resets it to 10px, 10px.
- A, pressed (
:active): the frame moves 60% of the shadow offset toward the shadow and the shadow shrinks to 40%, so it reads as pressed into the board. - B, hover the sticky note: it straightens from 7° to 4° and rises 3px.
- B, click or Enter/Space on the note: it peels up to
rotate(-10deg) translate(8px, -150px)with a springy 380ms ease, the shadow grows from 5px to 9px 12px, the small label changes “peel me” → “stick back”, and the date “08 · 07 · 26” written on the frame is now visible. Click again to stick it back. - C, drag: the frame follows the hand by half the accumulated x travel (clamped ±40px, so ±20px of movement), tilts 0.12° per px, and every pixel of movement adds
(|dx| + |dy| × 0.5) / 3600to the development. Release springs it back over 420ms. - C, keyboard: Space or Enter adds 0.10 and plays a 360ms side-to-side nudge.
- C at 100%: the caption “Gran’s tomatoes, again” reveals left to right over 900ms by
clip-path. The meter fills black. “Eject new” resets development to 0. - Reduced motion: no transitions, no nudge keyframes, C does not move while shaken but still develops.
Structure
1280 × 800, body padding 36px 48px 40px, dotted board (22px grid of 1px dots)
CONTACT SHEET 03 — POLAROID FRAMES Three treatments for one 300px frame. Move, peel, shake.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2px rule
┌───────────┐▌ ▒▒tape▒▒ ┌───────────┐
│ desert │▌ ┌───────────┐ │ milky │
│ road │▌ │ lake + │ │ tomato │
│ 268×268 │▌ │ red coat │ │ vines │
│ │▌ │ ┌────────┐ │ │
│Route 9, │▌ │Juno at│ sticky │ │ (caption │
└───────────┘▌ └───────│ 152px │ └───────────┘
▀▀▀▀▀▀▀▀▀▀▀▀ └────────┘
[A] Hard shadow [B] Tape + note [C] Developing
▬▬▬▬▬▬ 35% [EJECT NEW]
rotate −3° rotate 2.5° rotate −1.5°
headerwith anh1(mono caps) and a one-linep, 2px ink bottom rule.main.sheet: gridrepeat(3, minmax(0, 300px)), gap 72px, centred both ways.- Each
.cell: grid, gap 34px. Afigure.poland ap.labelwith a 24px boxed letter. .pol:background: --paper, 2px ink border,padding: 16px 16px 0. Inside:.img(aspect 1, 2px ink border, an inline SVG scene) andfigcaption.cap(68px tall).- A:
.pol.hard,tabindex="0",role="group". - B:
.pol.tapedwithspan.tape(absolute, top −17px, 116 × 34, centred, rotate −5°),span.under(the date, bottom right of the frame), andbutton.sticky(absolute, right −42px, bottom −34px, 152px wide, min-height 132px). - C:
.pol.dev,tabindex="0",role="group",--pcustom property..metersits absolutely 14px under the label, so the three labels align on one baseline.
Image scenes (inline SVG, viewBox="0 0 100 100", preserveAspectRatio="xMidYMid slice"): A is a desert road at sunrise (peach sky, cream sun, rust mesas, dark road with a yellow dashed line). B is a person in a tomato coat seen from behind at a blue lake. C is tomato vines (greens with five red tomatoes and highlights). No photos.
Tokens
:root {
--bg: #f1eee4; /* board */
--paper: #fffdf6; /* frame */
--ink: #141414; /* borders, hard shadows, text */
--ink-2: #4a4740; /* secondary text */
--line: #d8d3c4;
--tomato: #ff4f2e; /* accent: focus ring, tape, A caption */
--sticky: #ffd93b; /* sticky note */
--film: #d9dccf; /* undeveloped haze */
--marker: "Permanent Marker", "Marker Felt", cursive;
--mono: "Sometype Mono", ui-monospace, monospace;
--frame-w: 300px; --frame-pad: 16px; --cap-h: 68px; --border: 2px;
--shadow-hard: 10px; /* default offset, both axes */
--shadow-max: 14px;
--dots: radial-gradient(rgba(20,20,20,.13) 1px, transparent 1.2px) 0 0 / 22px 22px;
--ease: cubic-bezier(.2, .7, .2, 1);
--spring: cubic-bezier(.34, 1.56, .64, 1);
--t-shadow: 140ms; --t-peel: 380ms; --t-return: 420ms; --t-caption: 900ms; --t-nudge: 360ms;
}
Typography
| Role | Family | Size | Weight | Line-height | Tracking | Case |
|---|---|---|---|---|---|---|
| Header title | Sometype Mono | 13px | 600 | 1 | 0.16em | UPPER |
| Header line | Sometype Mono | 12px | 400 | 1.5 | 0.04em | sentence, --ink-2 |
| Caption | Permanent Marker | 24px | 400 | 1.1 | 0 | as written, rotate −1.5° |
| A caption colour | — | — | — | — | — | --tomato |
| Sticky note | Permanent Marker | 17px | 400 | 1.2 | 0 | as written |
| Sticky label | Sometype Mono | 10px | 600 | 1 | 0.12em | UPPER |
| Under-note date | Permanent Marker | 14px | 400 | 1 | 0 | --ink-2, rotate −3° |
| Cell label | Sometype Mono | 12px | 400/600 | 1.5 | 0.08em | UPPER, tail --ink-2 |
| Meter value | Sometype Mono | 13px | 600 | 1.5 | 0 | — |
| Button | Sometype Mono | 11px | 400 | 1 | 0.1em | UPPER |
Marker is for handwriting on the objects only. Every UI label is mono.
Motion
| Element | Trigger | Property | From → To | Duration | Easing | Reduced motion |
|---|---|---|---|---|---|---|
| A shadow | pointer move (rAF) | box-shadow offset | current → away from pointer, 6–14px | 140ms | --ease | instant |
| A frame | :active | translate, shadow | → 60% toward shadow, shadow 40% | 160ms | --ease | instant |
| Sticky | hover | transform | rotate 7° → 4°, −3px | 380ms | --spring | instant |
| Sticky | click | transform, shadow | → rotate −10°, (8px, −150px), shadow 9px 12px | 380ms | --spring | instant |
| C frame | drag | translate, rotate | follows hand, ±20px, 0.12°/px | none | — | stays still |
| C frame | release | transform | → rotate −1.5° | 420ms | --spring | instant |
| C frame | Space / Enter | keyframes | −8px/−4° → +7px/1.5° → rest | 360ms | --ease | none |
| C image | --p changes | blur, saturate, contrast, haze opacity | see formula | live | — | same |
| C caption | --p reaches 1 | clip-path: inset(0 100% 0 0 → 0) | hidden → shown | 900ms | --ease | instant |
Development formula, all from one custom property --p (0–1):
.dev .img svg { filter: blur(calc((1 - var(--p)) * 7px)) saturate(calc(var(--p) * 1.1)) contrast(calc(.55 + var(--p) * .45)); }
.dev .img::after { background: var(--film); opacity: calc((1 - var(--p)) * .94); }
States
- A resting: shadow 10px, 10px; frame rotate −3°; cursor
crosshair. - A focus-visible: 2px tomato outline, 4px offset.
- Sticky resting: yellow, 2px ink border, 5px 5px hard shadow, folded corner (22px triangle, darker yellow
#e6bf1f).aria-pressed="false". - Sticky peeled:
aria-pressed="true", label “stick back”, larger shadow. - C developing: cursor
grab; while dragging.shaking, cursorgrabbing, transition off. - C done (
.done): caption visible; the haze is gone. - Meter: 10px tall, 2px ink border, black fill to
--p × 100%. “Eject new” button: 40px tall, 3px hard shadow, presses flat on:active. - No disabled, loading, or error states.
Accessibility
- A:
figurewithrole="group"andaria-label="Hard shadow polaroid. Arrow keys move the light."; arrows callpreventDefault(). - B: the sticky note is a real
buttonwitharia-pressedand a label that contains its text: “Sticky note: the water was freezing, she went in twice. Press to peel it up.” - C:
role="group", label updated with the percentage: “Developing polaroid, 35 percent developed. Drag it side to side, or press Space, to shake it.” The label text under it is linked witharia-describedby. The percentage is anoutputelement. - One polite live region announces: each 25% step, “Developed: Gran’s tomatoes, again.”, the peel state, and “New shot ejected. Blank film.”
- Image SVGs are
aria-hidden; captions are realfigcaptiontext. - Contrast:
--inkon--paper18:1;--ink-2on--bg8:1; tomato caption on paper is large display text (24px) at 3.2:1, used decoratively next to the same words as the label. - Hit targets: frames 300px, sticky 152 × 132, button 40px tall.
Responsive rules
- ≥ 1280: three 300px columns, 72px gaps.
- 1024 (861–1100): columns
minmax(0, 260px), gap 48px, captions 20px. - 768 and < 860: one column of 300px frames, 72px row gap, header stacks (title over line), body padding 28px 20px. The sticky moves in to
right: -12pxso it stays on screen. - 375: same single column; nothing overflows. The page scrolls vertically.
- Pointer-light on A is mouse only; on touch, the shadow stays at its last value. Shaking works with touch because C has
touch-action: none.
Acceptance checklist
Always
- One frame component (16px borders, square image, 68px caption strip, 2px ink border) used three times with modifier classes.
- Hard shadow is a zero-blur
box-shadowwhose offset points away from the cursor, length 6–14px, updated once per frame. - Arrow keys move the hard shadow; Home resets it.
- Tape is a semi-transparent striped strip with zig-zag ends from
clip-path, usingmix-blend-mode: multiply. - Sticky note is a button with
aria-pressed; peeling reveals something underneath. - Development is a single
--pcustom property driving blur, saturation, contrast and haze opacity. - Shaking by drag and by Space both develop the print; the caption reveals at 100%.
- Labels under all three cells share one baseline.
- Reduced motion keeps every interaction but removes movement.
This demo
- Board
#f1eee4with a 22px dot grid; frames#fffdf6; ink#141414. - Captions “Route 9, 6:40am” (tomato), “Juno at Kettle Pond”, “Gran’s tomatoes, again”.
- Sticky reads “the water was freezing. she went in twice.” and the date under it is “08 · 07 · 26”.
- C starts at 35%; a drag of about 3600px of travel takes it to 100%.
- Frames rotate −3°, 2.5°, −1.5°.
Implementation notes
Light from the cursor. The shadow is the unit vector from pointer to frame centre, scaled:
addEventListener('pointermove', e => {
if (e.pointerType !== 'mouse') return;
px = e.clientX; py = e.clientY;
if (raf) return;
raf = requestAnimationFrame(() => {
raf = 0;
const r = hard.getBoundingClientRect();
const dx = r.left + r.width / 2 - px, dy = r.top + r.height / 2 - py;
const d = Math.hypot(dx, dy) || 1, len = Math.min(14, 6 + d / 60);
hard.style.setProperty('--sx', (dx / d * len).toFixed(1) + 'px');
hard.style.setProperty('--sy', (dy / d * len).toFixed(1) + 'px');
});
});
Shake to develop. Distance travelled, not speed, drives progress, so a slow wobble still works:
dev.addEventListener('pointermove', e => {
if (!down) return;
const dx = e.clientX - lx, dy = e.clientY - ly; lx = e.clientX; ly = e.clientY;
ox = clamp(ox + dx, 40);
if (!reduce) dev.style.transform = `translate(${ox * .5}px, ${clamp(dy, 6)}px) rotate(${-1.5 + ox * .12}deg)`;
setP(p + (Math.abs(dx) + Math.abs(dy) * .5) / 3600);
});
Washi tape ends.
.tape { background: repeating-linear-gradient(135deg, rgba(255,79,46,.62) 0 7px, rgba(255,79,46,.42) 7px 14px);
clip-path: polygon(0 8%,4% 0,8% 10%,12% 2%,88% 2%,92% 10%,96% 0,100% 8%,100% 92%,96% 100%,92% 90%,88% 98%,12% 98%,8% 90%,4% 100%,0 92%);
mix-blend-mode: multiply; }
Common mistakes:
- A blurred drop shadow on A. The point is a hard, zero-blur offset.
- Animating
filterper frame with JS. Set--pand let CSS compute every filter value. - Putting the meter in the label’s grid row, which pushes C’s label below A and B. Position it absolutely.
- Sticky note as a
divwith a click handler. It must be a button. - Starting C at 0%. The first frame should hint at the picture, so start at 35%.
- Real photos. Every image is a few flat SVG shapes.
Rebuild order:
- Board, header, three-cell grid.
- One frame component with an SVG scene and caption.
- A: hard shadow variables, pointer and keys.
- B: tape, sticky button, peel state, the date under it.
- C:
--pfilters and haze, drag to shake, Space nudge, caption reveal, meter, reset. - Live region, labels, reduced motion, single-column layout.
Details
- Palette
- Type
- Permanent Marker · Sometype Mono
- Motion
- Subtle motion
- Build
- One session
- Tags
On the shelves