Nº 356 of 520
Photo post with carousel
A dark square photo post: four CSS-painted slides with drag, dots and counter, double-tap heart pop, save toggle, caption with more, and inline comments.
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 "Photo post with carousel" (piece social-photo-post) 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º 356 · "Photo post with carousel" · www.designlounge.live -->
# Photo post with 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
A single post from a square-photo feed, for an invented network called Weft, in a dark theme. A ceramicist's post holds four slides: a blue glazed bowl, an open kiln, a grid of glaze test tiles, and clay on the wheel. Every "photo" is painted with CSS gradients, so the piece ships with no images. It reads at a glance as the familiar format: story-ring avatar, handle with Follow, square media with a 1/4 pill, heart-comment-send on the left, dots in the middle, save on the right, then likes, caption with "more", and an add-comment row. The detail worth copying is the double-tap: a big white heart pops at the exact point you tapped, while a tap-drag swipes slides, and the two never confuse each other.
## Reference behaviour
1. First frame: slide 1 (the bowl), counter "1/4", first dot coral, Next arrow present but hidden until hover, Prev absent. Likes "Liked by tiago.m and 2,318 others".
2. Hover the photo: round 32px Prev/Next arrows fade in at the left and right edges. Prev is removed on slide 1, Next on slide 4.
3. Drag the photo horizontally: the track follows the pointer 1:1 with no transition. Past the first or last slide it follows at 30% (rubber band). Release: if the drag passed 20% of the width or moved faster than 0.5px/ms, go to the neighbouring slide; else snap back. Snap is 420ms expo-out.
4. Arrow keys on the focused photo move slides. The counter and dots follow; the active dot is coral and 1.2× size.
5. Double-tap the photo (two taps within 300ms, each moving less than 8px): a 96px white heart pops at the tap point, scales 0.2 → 1.15 → 0.95 → 1, holds, then floats up 30px and fades over 900ms. If the post was not liked, the like button fills red with a bump and the count ticks to 2,319. Double-tapping an already-liked post replays the heart but does not unlike.
6. Press L on the focused photo: same as a double-tap at the centre.
7. Click the heart button: toggles like and unlike. No big heart.
8. Click Save: bookmark fills, bumps, toast "Saved to Glazes". Again: "Removed from Glazes".
9. Click Follow: text becomes "Following" in muted ink.
10. Click "more": the rest of the caption appears inline with two hashtags and focus moves to it. The ellipsis and button are removed.
11. Type in "Add a comment…": Post enables. Submit: the comment appears under the caption as "you" with a rise-in, and "View all 86 comments" becomes 87.
12. Click the comment icon: focuses the comment field.
## Structure
```
stage 1280×800, dark radial, card centred
┌────────── card 440px, radius 20, 1px line, overflow hidden ──────────┐
│ (38 ring) noor.kiln · Follow [ ⋯ ] │ 62
│ Kiln Lane Studio, Porto │
├──────────────────────────────────────────────────────────────────────┤
│ [1/4] │
│ [‹] slide: CSS-painted photo, 1:1 (440×440) [›] │
│ ♥ heart pop at tap point │
├──────────────────────────────────────────────────────────────────────┤
│ [like][cmt][send] • • • • [save] │ 46
│ Liked by tiago.m and 2,318 others │
│ noor.kiln Third firing of the tidepool series… more │
… 186 more lines. Copy or download for the full brief. Loading the source…Brief
226 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 single post from a square-photo feed, for an invented network called Weft, in a dark theme. A ceramicist’s post holds four slides: a blue glazed bowl, an open kiln, a grid of glaze test tiles, and clay on the wheel. Every “photo” is painted with CSS gradients, so the piece ships with no images. It reads at a glance as the familiar format: story-ring avatar, handle with Follow, square media with a 1/4 pill, heart-comment-send on the left, dots in the middle, save on the right, then likes, caption with “more”, and an add-comment row. The detail worth copying is the double-tap: a big white heart pops at the exact point you tapped, while a tap-drag swipes slides, and the two never confuse each other.
Reference behaviour
- First frame: slide 1 (the bowl), counter “1/4”, first dot coral, Next arrow present but hidden until hover, Prev absent. Likes “Liked by tiago.m and 2,318 others”.
- Hover the photo: round 32px Prev/Next arrows fade in at the left and right edges. Prev is removed on slide 1, Next on slide 4.
- Drag the photo horizontally: the track follows the pointer 1:1 with no transition. Past the first or last slide it follows at 30% (rubber band). Release: if the drag passed 20% of the width or moved faster than 0.5px/ms, go to the neighbouring slide; else snap back. Snap is 420ms expo-out.
- Arrow keys on the focused photo move slides. The counter and dots follow; the active dot is coral and 1.2× size.
- Double-tap the photo (two taps within 300ms, each moving less than 8px): a 96px white heart pops at the tap point, scales 0.2 → 1.15 → 0.95 → 1, holds, then floats up 30px and fades over 900ms. If the post was not liked, the like button fills red with a bump and the count ticks to 2,319. Double-tapping an already-liked post replays the heart but does not unlike.
- Press L on the focused photo: same as a double-tap at the centre.
- Click the heart button: toggles like and unlike. No big heart.
- Click Save: bookmark fills, bumps, toast “Saved to Glazes”. Again: “Removed from Glazes”.
- Click Follow: text becomes “Following” in muted ink.
- Click “more”: the rest of the caption appears inline with two hashtags and focus moves to it. The ellipsis and button are removed.
- Type in “Add a comment…”: Post enables. Submit: the comment appears under the caption as “you” with a rise-in, and “View all 86 comments” becomes 87.
- Click the comment icon: focuses the comment field.
Structure
stage 1280×800, dark radial, card centred
┌────────── card 440px, radius 20, 1px line, overflow hidden ──────────┐
│ (38 ring) noor.kiln · Follow [ ⋯ ] │ 62
│ Kiln Lane Studio, Porto │
├──────────────────────────────────────────────────────────────────────┤
│ [1/4] │
│ [‹] slide: CSS-painted photo, 1:1 (440×440) [›] │
│ ♥ heart pop at tap point │
├──────────────────────────────────────────────────────────────────────┤
│ [like][cmt][send] • • • • [save] │ 46
│ Liked by tiago.m and 2,318 others │
│ noor.kiln Third firing of the tidepool series… more │
│ (your comments) │
│ View all 86 comments │
│ 2 HOURS AGO │
├──────────────────────────────────────────────────────────────────────┤
│ Add a comment… Post │ 52
└──────────────────────────────────────────────────────────────────────┘
- Card:
articlelabelled by the handle. - Photo:
div role="region" aria-roledescription="carousel" tabindex="0"with a label that names the keys. Each slide isrole="group" aria-roledescription="slide"with “n of 4: description”. Off-screen slides getaria-hidden="true". - Arrows: buttons with “Previous photo” / “Next photo”, hidden with the
hiddenattribute at the ends. - Action bar: four icon buttons and a decorative dots strip.
- Caption:
pwith the handle in bold; “more” is a button witharia-expandedandaria-controls. - Comment row:
formwith a visually hidden label, an input, and a submit button. - Toast:
role="status"live region.
Tokens
:root {
--stage: #0b0b0c; /* page; behind a radial of #17151a at 50% 40% */
--card: #141416;
--raise: #1d1d20; /* comment row on focus */
--line: #28282c;
--ink: #f2efe9; /* warm white, never #fff for text */
--ink-2: #b4b0a9;
--ink-3: #8d8983; /* meta, placeholders, idle dots */
--accent: #ff6a4d; /* Follow, active dot, Post, focus */
--accent-2: #ffb547; /* second stop of the story ring */
--heart: #ff4f5e; /* liked heart */
--sans: "Manrope", system-ui, sans-serif;
--serif: "Instrument Serif", Georgia, serif; /* only inside painted photos */
--r: 20px;
--w: 440px;
--t-micro: 160ms;
--t-slide: 420ms;
--t-heart: 900ms;
--ease: cubic-bezier(.2,.7,.2,1);
--ease-out: cubic-bezier(.16,1,.3,1);
--ease-pop: cubic-bezier(.34,1.56,.64,1);
}
Card shadow 0 30px 60px -30px rgba(0,0,0,.8). Story ring: conic-gradient(from 200deg, --accent, --accent-2, --accent), 2px padding, then a 2px card-coloured border on the avatar.
Typography
| Role | Family | Size | Weight | Notes |
|---|---|---|---|---|
| Handle | Manrope | 14px | 700 | header and caption |
| Follow | Manrope | 14px | 700 | coral; “Following” muted |
| Location | Manrope | 12px | 400 | --ink-2 |
| Likes line | Manrope | 14px | 700 | |
| Caption | Manrope | 14px/1.45 | 400 | |
| “more”, View all | Manrope | 14px | 600 / 500 | --ink-3 |
| Age | Manrope | 11px | 400 | uppercase, 0.06em |
| Counter pill | Manrope | 12px | 600 | 0.02em, on 62% black |
| Photo captions | Instrument Serif italic | 20–22px | 400 | “Tidepool, cone 10”, “Throwing day” |
The serif lives inside the pictures, as if hand-labelled. The UI chrome stays in Manrope.
Motion
| Thing | Trigger | Property | From → to | Duration / easing | Reduced motion |
|---|---|---|---|---|---|
| Slide | arrow, key, release | track translateX | −i×100% → −j×100% | 420ms --ease-out | 1ms |
| Drag follow | pointermove | translateX | 1:1, 0.3 past ends | none | same |
| Arrows | hover / focus | opacity | 0 → 1 | 160ms | instant |
| Dot | slide change | colour, scale | grey 0.55 → coral 1.2 | 240ms --ease | instant |
| Heart pop | double-tap, L | scale, rotate, opacity, y | 0.2/−12° → 1.15/4° → 0.95 → 1 → 1.1/−30px/0 | 900ms --ease-out | not shown |
| Like / save icon | toggle on | scale | 0.6 → 1.25 → 1 | 420ms --ease-pop | instant |
| Likes number | change | translateY, opacity | 60%/0 → 0/1 | 320ms --ease-out | instant |
| New comment | submit | same as likes number | 320ms | instant | |
| Toast | message | opacity, y | 0/14px → 1/0 | 280ms, hides at 1.8s | instant |
States
- Arrow hidden: absent from layout at the ends (
hidden), so it is also out of the tab order. - Like on: red stroke and fill,
aria-pressed="true", label “Unlike”. - Save on: filled bookmark,
aria-pressed="true", label “Remove from saved”. - Follow on: “Following”,
aria-pressed="true",--ink-2. - Post disabled: 40% opacity until the field has non-space text.
- Comment row focus-within: background
--raise. - Photo focus-visible: coral outline inset by 3px so it shows inside the square.
- Dragging: cursor
grabbing, track transition off.
Accessibility
- The carousel is one tab stop. Left/Right move slides; L likes. The region label says so.
- The visible “1/4” pill is
aria-hidden; the slide group label carries “n of 4” plus a description of the painting. - Double-tap is never the only way: the heart button and the L key do the same.
- Icon buttons: Like/Unlike, Comment, Share, Save/Remove from saved, More options, Previous photo, Next photo.
- The comment input has a real
label(visually hidden). - Contrast:
#f2efe9on#141416is 15:1;#8d8983is 5.3:1; coral on card is 6.5:1. - Icon buttons are 40×40; arrows are 32px visible inside a 440px image, and the image itself is the larger swipe target.
Responsive rules
- ≥1280 through 768: card fixed at 440px, centred. The painted photos are in percentages, so they scale with the square.
- <640: card is
min(440px, 100%)with 16px page padding. At 375 the square is 341px; everything still fits on one line except the caption, which wraps. - If the caption is expanded and the card outgrows the viewport, the page scrolls vertically; it never scrolls sideways.
- Touch:
touch-action: pan-yon the photo so vertical scroll still works while horizontal drag swipes.
Acceptance checklist
Always
- Header: ring avatar, handle, Follow, location, more button.
- Media is 1:1 with a counter pill, edge arrows on hover, and a dot per slide below.
- Drag follows the pointer, rubber-bands at the ends, and snaps by distance (20%) or speed (0.5px/ms).
- Double-tap within 300ms pops a heart at the tap point and likes; it never unlikes.
- A drag longer than 8px never counts as a tap.
- Heart and Save are
aria-pressedtoggles with a bump animation. - Caption truncates with “more”, which expands inline and moves focus.
- Post is disabled until there is text; submitting adds the comment and bumps the count.
- Reduced motion: no heart pop, slides change instantly.
- No horizontal scroll at 375px.
This demo
- Handle noor.kiln at Kiln Lane Studio, Porto; four slides: bowl, kiln, tiles, wheel.
- Likes start at 2,318 and become 2,319 after a double-tap.
- Save toast reads “Saved to Glazes”.
- Comments read “View all 86 comments” and become 87 after posting.
- Slide 3 caption reads “Tidepool, cone 10” in italic serif.
Implementation notes
Tap, double-tap and drag on one surface. Track the pointer from down to up. If it moved more than 8px it was a drag; otherwise compare with the previous tap time.
frame.addEventListener('pointerup', e => {
track.classList.remove('drag');
const w = frame.clientWidth, v = Math.abs(dx) / (performance.now() - t0);
if (Math.abs(dx) > 8) { // a swipe, never a tap
go(Math.abs(dx) > w * .2 || v > .5 ? i - Math.sign(dx) : i);
return;
}
go(i);
const now = performance.now(), r = frame.getBoundingClientRect();
if (now - lastTap < 300) { like(true, e.clientX - r.left, e.clientY - r.top); lastTap = 0; }
else lastTap = now;
});
Rubber band. During the drag, scale the offset when pulling past an end.
const edge = (i === 0 && dx > 0) || (i === N - 1 && dx < 0) ? .3 : 1;
track.style.transform = `translateX(calc(${-i * 100}% + ${dx * edge}px))`;
Painting a photo with CSS. The bowl is one div: a bottom-rounded half ellipse with a radial glaze, plus ::before for the rim and ::after for a soft highlight.
.bowl { position:absolute; left:20%; top:46%; width:60%; height:32%;
border-radius: 0 0 50% 50% / 0 0 100% 100%;
background: radial-gradient(120% 90% at 30% 10%, #7fc3d2, #1f6f8b 40%, #0f3f57 70%, #0a2533); }
.bowl::before { content:""; position:absolute; left:0; right:0; top:-14%; height:28%; border-radius:50%;
background: radial-gradient(closest-side, #0c3346 60%, #1c5f78 80%, #c9dfe0 96%, transparent); }
Common mistakes.
- Using
dblclick. It fires after two clicks that may include a drag, and is unreliable on touch. - Letting a double-tap toggle the like off. It only ever likes.
- Centring the big heart instead of placing it at the tap point.
- Leaving off-screen slides readable by screen readers.
- Copying the real app’s gradient logo or the camera glyph. Weft is coral and amber.
- Pure
#fffbody text on the dark card; use the warm--ink.
Rebuild order.
- Card shell, header with ring avatar.
- Square frame, track, four painted slides.
- Arrows, counter, dots, keyboard.
- Pointer drag with rubber band and snap.
- Double-tap heart and like state.
- Save, Follow, toast.
- Caption “more”, comment form.
- Reduced motion and 375px check.
Details
- Palette
- Type
- Manrope · Instrument Serif
- Motion
- Rich motion
- Build
- One session
- Tags
On the shelves