Design Lounge
Install

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

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

Brief

226 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 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                  │
│ (your comments)                                                      │
│ View all 86 comments                                                 │
│ 2 HOURS AGO                                                          │
├──────────────────────────────────────────────────────────────────────┤
│ Add a comment…                                               Post    │ 52
└──────────────────────────────────────────────────────────────────────┘
  • Card: article labelled by the handle.
  • Photo: div role="region" aria-roledescription="carousel" tabindex="0" with a label that names the keys. Each slide is role="group" aria-roledescription="slide" with “n of 4: description”. Off-screen slides get aria-hidden="true".
  • Arrows: buttons with “Previous photo” / “Next photo”, hidden with the hidden attribute at the ends.
  • Action bar: four icon buttons and a decorative dots strip.
  • Caption: p with the handle in bold; “more” is a button with aria-expanded and aria-controls.
  • Comment row: form with 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

RoleFamilySizeWeightNotes
HandleManrope14px700header and caption
FollowManrope14px700coral; “Following” muted
LocationManrope12px400--ink-2
Likes lineManrope14px700
CaptionManrope14px/1.45400
“more”, View allManrope14px600 / 500--ink-3
AgeManrope11px400uppercase, 0.06em
Counter pillManrope12px6000.02em, on 62% black
Photo captionsInstrument Serif italic20–22px400“Tidepool, cone 10”, “Throwing day”

The serif lives inside the pictures, as if hand-labelled. The UI chrome stays in Manrope.

Motion

ThingTriggerPropertyFrom → toDuration / easingReduced motion
Slidearrow, key, releasetrack translateX−i×100% → −j×100%420ms --ease-out1ms
Drag followpointermovetranslateX1:1, 0.3 past endsnonesame
Arrowshover / focusopacity0 → 1160msinstant
Dotslide changecolour, scalegrey 0.55 → coral 1.2240ms --easeinstant
Heart popdouble-tap, Lscale, rotate, opacity, y0.2/−12° → 1.15/4° → 0.95 → 1 → 1.1/−30px/0900ms --ease-outnot shown
Like / save icontoggle onscale0.6 → 1.25 → 1420ms --ease-popinstant
Likes numberchangetranslateY, opacity60%/0 → 0/1320ms --ease-outinstant
New commentsubmitsame as likes number320msinstant
Toastmessageopacity, y0/14px → 1/0280ms, hides at 1.8sinstant

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: #f2efe9 on #141416 is 15:1; #8d8983 is 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-y on 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-pressed toggles 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 #fff body text on the dark card; use the warm --ink.

Rebuild order.

  1. Card shell, header with ring avatar.
  2. Square frame, track, four painted slides.
  3. Arrows, counter, dots, keyboard.
  4. Pointer drag with rubber band and snap.
  5. Double-tap heart and like state.
  6. Save, Follow, toast.
  7. Caption “more”, comment form.
  8. Reduced motion and 375px check.

Details

Palette
Type
Manrope · Instrument Serif
Motion
Rich motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Tailwind CSS docs

A control. It studies this site for quiet, not for a show.

All sources

On the shelves