Design Lounge
Install

Nº 446 of 520

Testimonials masonry wall

A riso-paper wall of twelve Pressroom letters in three CSS columns: one coral featured quote, marked phrases, post cards, and a fade with Show more.

Preview

Open full size
1280 × 800 · 12.8 KB · 195 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 "Testimonials masonry wall" (piece testimonials-masonry-wall) 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

277 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 testimonials section for Pressroom, a print proofing tool for small studios. Twelve short letters sit in a 3-column masonry made with CSS columns, not a grid. The cards come in four kinds: a big coral featured quote, a plain quote, a quote with one coral marked phrase, and a post card with a handle and a date. The wall is cut off at the bottom by a soft fade, and a Show more letters button opens the rest. It looks like a riso print: cream paper with a dot grain, black ink, one fluoro coral. The detail worth copying is the mix of card kinds inside one column flow, so the wall reads as a pinboard and not a row of identical boxes.

This is not testimonials-wall-grid. That one is a fixed CSS grid with spans and a hover lift. This one is free column flow, a fade, and an expand.

Reference behaviour

  1. First frame: a header row and the wall. The header has a kicker “LETTERS TO PRESSROOM” with a 10px coral dot, a 56px serif heading “Proofed on Thursday, printed on Friday.”, and on the right a tally: “212” at 40px over “studios writing back / Avg. 4.8 / 5 · since 2023”.
  2. The word printed is italic with a coral band behind its lower 30%. It is the only coral in the heading.
  3. Under a 1.5px black rule, the wall starts. Column 1 starts with the coral featured card. Columns 2 and 3 start with plain and marked quotes. A post card is visible in the first frame, partly faded.
  4. The wall is capped at calc(100vh - 262px), never less than 420px. The bottom 220px fades out with a mask, so the paper grain shows through the fade.
  5. Under the wall, a row: a 1.5px black line, the button, a 1.5px black line. The button overlaps the fade by 28px.
  6. Click Show more letters: the cap lifts to 3200px over 560ms with expo-out. The fade is removed. The chevron turns 180°. The label becomes Show fewer. aria-expanded becomes true.
  7. Click Show fewer: the cap comes back, the fade returns, the label resets, and the wall scrolls back to its top so the reader is not left below a short section.
  8. Hover the button: it moves up-left 2px and its coral offset shadow grows from 4px to 6px. Press: it moves down-right 2px and the shadow drops to 1px.
  9. Cards do not move, flip, or link. They are text.
  10. Reduced motion: the height change is instant, the chevron turns with no transition, and the button does not move on hover. Smooth scroll on collapse becomes instant.

Structure

1280 × 800, section padding 40px 56px 32px, max-width 1280px
┌──────────────────────────────────────────────────────────────────────┐
│ ● LETTERS TO PRESSROOM                                         212   │
│ Proofed on Thursday, printed on Friday.     studios writing back     │
│ 56px serif                                  Avg. 4.8 / 5 · since 2023│
├══════════════════════════════════════════════════════════════════════┤ 1.5px ink rule, 20px under
│ ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐        │ 24px gap
│ │ FEATURED coral  │  │ plain quote     │  │ marked quote    │        │
│ │ 34px serif      │  │                 │  │                 │        │
│ │ 6px ink shadow  │  ├─────────────────┤  ├─────────────────┤        │
│ └─────────────────┘  │ marked quote    │  │ plain quote     │        │
│ ┌─────────────────┐  │                 │  ├─────────────────┤        │
│ │ post card       │  ├─────────────────┤  │ post card       │        │
│ │ (fading)        │  │ post (fading)   │  │ (fading)        │        │
│ └ ─ ─ ─ ─ ─ ─ ─ ─ ┘  └ ─ ─ ─ ─ ─ ─ ─ ─ ┘  └ ─ ─ ─ ─ ─ ─ ─ ─ ┘        │ mask fade 220px
│ ─────────────────────[ SHOW MORE LETTERS v ]──────────────────────── │ 44px button
└──────────────────────────────────────────────────────────────────────┘
  3 columns, column-gap 20px, card margin-bottom 20px
  • <section aria-labelledby> wraps everything. The heading is an h2 because this is a page section.
  • .head is a two-column grid: minmax(0,1fr) auto. Left: kicker p and h2. Right: tally p.
  • .wall is the clip box. It holds max-height, overflow: hidden, and the mask. It has an id for aria-controls.
  • .cols sits inside .wall and holds columns: 3. Keep these two elements apart. See Implementation notes.
  • Plain, marked and featured cards are figure > blockquote + figcaption.
  • Post cards are article with aria-label="Post by <name>", a header (avatar, name, handle), the text, and a footer (date, reply count).
  • .more is a flex row with the button and two pseudo-element lines.

Tokens

:root {
  /* colour */
  --paper: #f2ead9;        /* page */
  --card: #fbf6ea;         /* card surface */
  --ink: #16130f;          /* text, borders, rules */
  --ink-2: #4a443b;        /* kicker, tally, secondary */
  --ink-3: #6b6457;        /* role, handle, date */
  --rule: #d8ccb4;         /* dashed divider inside cards */
  --coral: #ff5b4a;        /* the one accent */
  --focus: #ff5b4a;

  /* type */
  --serif: "Instrument Serif", Georgia, serif;
  --mono: "DM Mono", ui-monospace, monospace;
  --fs-display: 56px;
  --fs-featured: 34px;
  --fs-quote: 21px;
  --fs-post: 19px;
  --fs-meta: 12px;
  --fs-name: 13px;

  /* space */
  --s-1: 4px; --s-2: 8px; --s-3: 12px; --s-4: 16px; --s-5: 20px; --s-6: 24px;
  --pad-x: 56px;
  --col-gap: 20px;

  /* shape */
  --border: 1.5px solid var(--ink);
  --radius: 0;
  --shadow-feat: 6px 6px 0 var(--ink);
  --shadow-btn: 4px 4px 0 var(--coral);

  /* motion */
  --ease: cubic-bezier(0.2, 0.7, 0.2, 1);
  --expo: cubic-bezier(0.16, 1, 0.3, 1);
  --t-micro: 160ms;
  --t-fade: 240ms;
  --t-expand: 560ms;
}

The paper grain is two dot layers on body:

background-image:
  radial-gradient(rgba(22,19,15,.09) .7px, transparent .8px),
  radial-gradient(rgba(255,91,74,.07) .7px, transparent .8px);
background-size: 4px 4px, 7px 7px;
background-position: 0 0, 2px 3px;

Typography

RoleFamilySizeWeightLine-heightTrackingCase
KickerDM Mono12px4001.450.14emupper
HeadingInstrument Serif56px4000.98-0.015emsentence
Tally numberInstrument Serif40px40010—
Tally textDM Mono12px4001.70.04emsentence
Featured quoteInstrument Serif34px4001.06-0.015emsentence
QuoteInstrument Serif21px4001.22-0.005emsentence
Post textInstrument Serif19px4001.280sentence
NameDM Mono13px5001.450—
Role · companyDM Mono12px4001.450—
Avatar initialsDM Mono12px50010.02emupper
Featured tagDM Mono11px40010.14emupper
ButtonDM Mono13px50010.08emupper

Rules:

  • Quotes are always serif. Everything else is mono. Do not set a name in serif.
  • The featured opening mark is a serif “ at 112px with line-height 0.6, in a 46px tall box so it does not push the text down.
  • Role text is one line with ellipsis. Never let it wrap under the avatar.

Motion

ThingTriggerPropertyFrom → toDurationEasingReduced motion
WallShow moremax-heightcalc(100vh - 262px) → 3200px560msexpoinstant
WallShow fewermax-height3200px → cap560msexpoinstant
FadeShow moremaskon → noneinstant—same
Chevrontogglerotate0 → 180deg240msstandardinstant
Buttonhovertranslate, shadow0 → -2px, 4px → 6px160msstandardno move
Buttonactivetranslate, shadow→ 2px, → 1px160msstandardno move
ScrollShow fewerscrollIntoView—browsersmoothauto

There is no entrance animation. The first frame is the finished wall.

States

  • Collapsed (default): cap on, mask fade on, aria-expanded="false", label “Show more letters”, chevron down.
  • Expanded: cap 3200px, no mask, aria-expanded="true", label “Show fewer”, chevron up. The button sits 8px under the last card.
  • Button hover: lift 2px up-left, coral shadow 6px.
  • Button active: push 2px down-right, coral shadow 1px.
  • Button focus-visible: 2px coral outline, offset 3px. The coral outline sits on cream, so it reads.
  • Card hover: nothing. Cards are not controls. Do not add a lift; that is the other wall.
  • Empty: if there are fewer than seven letters, drop the cap, the fade and the button. Show the cards in full.
  • Loading: not drawn. If letters load late, render the header and keep the wall at its 420px minimum.

Accessibility

  • Each quote card is a figure. The quote is a blockquote. The person is in figcaption.
  • Avatar discs are aria-hidden="true". The name is real text next to them.
  • Post cards are article with aria-label="Post by Mira Duensing". The handle is plain text, not a link.
  • The star row is role="img" with aria-label="5 out of 5". The five SVGs inside have no label of their own.
  • The button has aria-expanded and aria-controls="wall".
  • Cards hidden under the cap are still in the DOM and still read by a screen reader. That is fine because they hold no controls. If you add links inside cards, set inert on cards that are fully under the cap.
  • Contrast: #16130f on #fbf6ea is about 17:1. #6b6457 on #fbf6ea is about 5.4:1. Ink on coral #ff5b4a is about 6:1. Do not put white text on the coral.
  • The marked phrase has a 2px ink offset shadow as well as the coral fill, so it is not colour alone.
  • Button height 44px. Tab order: button only, since cards are not focusable.

Responsive rules

  • ≥1280: three columns, 56px side padding, heading 56px, cap calc(100vh - 262px).
  • 1024: same three columns. The cards narrow. Nothing else changes.
  • 768 (up to 1023): two columns, side padding 32px, heading 46px. The featured card stays first in column 1.
  • <640: one column, side padding 20px. The header stacks: heading 38px, then the tally on one line with “212” at 32px inline. Cap becomes calc(100vh - 300px). Featured quote drops to 28px.
  • At every width, scrollWidth equals the viewport. The clip box gets 8px of right padding and -8px margin so the 6px featured shadow is not cut.
  • Never switch to a horizontal scroll strip on phones. The wall stacks.

Acceptance checklist

Always

  • The wall is CSS columns, on an inner element, inside a separate clip element.
  • Cards use break-inside: avoid and never split across columns.
  • Four card kinds appear: featured, plain, marked phrase, post.
  • Exactly one featured card, and it is first in reading order.
  • The fade is a mask on the clip box, not a coloured overlay. The grain shows through.
  • The button toggles aria-expanded and its label, and is at least 44px tall.
  • Collapsing scrolls the wall back into view.
  • One accent colour. Coral is used for the featured card, marks, avatar fills, the button shadow and focus only.
  • Three columns at 1024 and up, two at 768, one under 640. No horizontal scroll at 390.
  • Reduced motion makes the expand instant and stops the button moving.

This demo

  • Heading reads “Proofed on Thursday, printed on Friday.” with printed italic on a coral band.
  • Tally reads “212” over “studios writing back” and “Avg. 4.8 / 5 · since 2023”.
  • Twelve letters. The featured one is Hana Vlasák, Founder · Kopf & Vlasák Press, tagged “STUDIO OF THE YEAR”.
  • Three post cards: @mira.prints (14 Mar 2026, 18 replies), @dele.makes.books (2 Feb 2026, 41 replies), @solherran (27 Jan 2026, 9 replies).
  • Three marked phrases: “right on the spread”, “what a bleed is”, “a week to an afternoon”.
  • Button reads “Show more letters”, then “Show fewer”.

Implementation notes

1. Keep the columns away from the cap. If columns and max-height sit on the same element, the browser does not cut the columns. It makes more columns to the right and hides them. Cards vanish sideways. Put the cap on a wrapper.

.wall {
  max-height: calc(100vh - 262px);
  min-height: 420px;
  overflow: hidden;
  padding-right: 8px; margin-right: -8px;   /* room for the 6px shadow */
  -webkit-mask-image: linear-gradient(#000 calc(100% - 220px), transparent calc(100% - 24px));
          mask-image: linear-gradient(#000 calc(100% - 220px), transparent calc(100% - 24px));
  transition: max-height 560ms var(--expo);
}
.wall.open { max-height: 3200px; -webkit-mask-image: none; mask-image: none; }
.cols { columns: 3; column-gap: 20px; }
.card { break-inside: avoid; margin: 0 0 20px; }

2. Do not fade with a paper-coloured gradient box. A flat #f2ead9 overlay hides the dot grain and shows a clear rectangle at the bottom. The mask fades the cards themselves.

3. The toggle. Keep it small. Scroll back on collapse.

const wall = document.getElementById('wall');
const btn = document.querySelector('.btn');
btn.addEventListener('click', () => {
  const open = btn.getAttribute('aria-expanded') !== 'true';
  btn.setAttribute('aria-expanded', String(open));
  wall.classList.toggle('open', open);
  btn.querySelector('span').textContent = open ? 'Show fewer' : 'Show more letters';
  if (!open) wall.scrollIntoView({
    block: 'start',
    behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth'
  });
});

4. The riso mark. A coral fill with a small ink offset, like two plates printed a hair apart.

mark { background: var(--coral); color: var(--ink); padding: 0 3px; box-shadow: 2px 1px 0 rgba(22,19,15,.85); }
h2 em { font-style: italic; background: linear-gradient(transparent 62%, var(--coral) 62% 92%, transparent 92%); padding: 0 4px; }

5. Card order sets the first frame. CSS columns fill top to bottom, then move right. Put the featured card first and a post card second, so column 1 shows both kinds. Check that columns 2 and 3 start with a plain and a marked quote. Reorder the source, not the visual order, so reading order still matches.

Common mistakes:

  • Using CSS grid with grid-row: span and calling it masonry. That is the other piece.
  • Equal-height cards. The point is uneven height.
  • A real social network logo on the post cards. Use a handle and a date only.
  • A second accent colour for the post cards.
  • Rounded corners. Corners here are square; the border is 1.5px ink.
  • Drop shadows on every card. Only the featured card and the button have an offset shadow.
  • Animating height: auto. Animate max-height between two numbers.
  • Leaving the fade on after expanding.
  • Hiding cards with display: none and showing them on click. The wall must keep its column flow when it opens.

Rebuild order:

  1. Paper background and grain.
  2. Header row with kicker, heading, tally, and rule.
  3. Clip box and inner columns with twelve cards.
  4. The four card kinds.
  5. Mask fade and the button row.
  6. Toggle script and scroll back.
  7. Breakpoints at 1023 and 639.
  8. Reduced motion.

Details

Palette
Type
Instrument Serif · DM Mono
Motion
Subtle motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Dragone

A show with a way in. Spectacle, then the ask.

All sources