Staggered Portrait Testimonial Grid
Faces first. When the endorsement is the person — founders, influencers, practitioners — this staggered portrait grid outperforms any card layout. Alternate columns are offset vertically with an nth-child rule, each portrait carries its quote in an overlay that lifts on hover, and the whole grid reveals in a diagonal cascade.
Published Updated
The code
<section class="tr-25" aria-label="Staggered portrait testimonial grid demo">
<div class="tr-25__stage">
<header class="tr-25__head">
<p class="tr-25__eyebrow">The people, not the logos</p>
<h2 class="tr-25__title">Eight founders. One sentence each.</h2>
</header>
<div class="tr-25__grid">
<figure class="tr-25__item" style="--tr-25-i:0" tabindex="0">
<img src="https://images.unsplash.com/photo-1573497019940-1c28c88b4f3e?q=80&w=600&auto=format&fit=crop" alt="Portrait of Alessandra Rossi" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Nineteen years of records moved in a weekend.”</blockquote><span><b>Alessandra Rossi</b>Vaultline Financial</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:1" tabindex="0">
<img src="https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?q=80&w=600&auto=format&fit=crop" alt="Portrait of Marcus Yeung" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Compliance stopped filing tickets and started filing praise.”</blockquote><span><b>Marcus Yeung</b>Halden Group</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:2" tabindex="0">
<img src="https://images.unsplash.com/photo-1517841905240-472988babdf9?q=80&w=600&auto=format&fit=crop" alt="Portrait of Grace Whitlock" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“94% of merged PRs now reference a token.”</blockquote><span><b>Grace Whitlock</b>Auryn</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:3" tabindex="0">
<img src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=600&auto=format&fit=crop" alt="Portrait of Dan Reyes" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Two data centres, four time zones, zero rollbacks.”</blockquote><span><b>Dan Reyes</b>Portside</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:4" tabindex="0">
<img src="https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=600&auto=format&fit=crop" alt="Portrait of Hana Sørensen" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Asked on Friday. Shipped Tuesday. With docs.”</blockquote><span><b>Hana Sørensen</b>Blomst</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:5" tabindex="0">
<img src="https://images.unsplash.com/photo-1534528741775-53994a69daeb?q=80&w=600&auto=format&fit=crop" alt="Portrait of Nia Osei" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Onboarding took an afternoon, not a quarter.”</blockquote><span><b>Nia Osei</b>Fieldwork</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:6" tabindex="0">
<img src="https://images.unsplash.com/photo-1580489944761-15a19d654956?q=80&w=600&auto=format&fit=crop" alt="Portrait of Erin McAllister" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Renewal was a fifteen-minute conversation.”</blockquote><span><b>Erin McAllister</b>Larkfield Health</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:7" tabindex="0">
<img src="https://images.unsplash.com/photo-1463453091185-61582044d556?q=80&w=600&auto=format&fit=crop" alt="Portrait of Samuel Idris" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“The first build took nine seconds. I ran it twice.”</blockquote><span><b>Samuel Idris</b>Ashcombe Labs</span></figcaption>
</figure>
</div>
<p class="tr-25__chip" aria-hidden="true">nth-child vertical offsets · aspect-ratio 3/4 portraits · scrim overlay · diagonal --i cascade</p>
</div>
</section><section class="tr-25" aria-label="Staggered portrait testimonial grid demo">
<div class="tr-25__stage">
<header class="tr-25__head">
<p class="tr-25__eyebrow">The people, not the logos</p>
<h2 class="tr-25__title">Eight founders. One sentence each.</h2>
</header>
<div class="tr-25__grid">
<figure class="tr-25__item" style="--tr-25-i:0" tabindex="0">
<img src="https://images.unsplash.com/photo-1573497019940-1c28c88b4f3e?q=80&w=600&auto=format&fit=crop" alt="Portrait of Alessandra Rossi" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Nineteen years of records moved in a weekend.”</blockquote><span><b>Alessandra Rossi</b>Vaultline Financial</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:1" tabindex="0">
<img src="https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?q=80&w=600&auto=format&fit=crop" alt="Portrait of Marcus Yeung" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Compliance stopped filing tickets and started filing praise.”</blockquote><span><b>Marcus Yeung</b>Halden Group</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:2" tabindex="0">
<img src="https://images.unsplash.com/photo-1517841905240-472988babdf9?q=80&w=600&auto=format&fit=crop" alt="Portrait of Grace Whitlock" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“94% of merged PRs now reference a token.”</blockquote><span><b>Grace Whitlock</b>Auryn</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:3" tabindex="0">
<img src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=600&auto=format&fit=crop" alt="Portrait of Dan Reyes" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Two data centres, four time zones, zero rollbacks.”</blockquote><span><b>Dan Reyes</b>Portside</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:4" tabindex="0">
<img src="https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=600&auto=format&fit=crop" alt="Portrait of Hana Sørensen" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Asked on Friday. Shipped Tuesday. With docs.”</blockquote><span><b>Hana Sørensen</b>Blomst</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:5" tabindex="0">
<img src="https://images.unsplash.com/photo-1534528741775-53994a69daeb?q=80&w=600&auto=format&fit=crop" alt="Portrait of Nia Osei" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Onboarding took an afternoon, not a quarter.”</blockquote><span><b>Nia Osei</b>Fieldwork</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:6" tabindex="0">
<img src="https://images.unsplash.com/photo-1580489944761-15a19d654956?q=80&w=600&auto=format&fit=crop" alt="Portrait of Erin McAllister" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Renewal was a fifteen-minute conversation.”</blockquote><span><b>Erin McAllister</b>Larkfield Health</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:7" tabindex="0">
<img src="https://images.unsplash.com/photo-1463453091185-61582044d556?q=80&w=600&auto=format&fit=crop" alt="Portrait of Samuel Idris" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“The first build took nine seconds. I ran it twice.”</blockquote><span><b>Samuel Idris</b>Ashcombe Labs</span></figcaption>
</figure>
</div>
<p class="tr-25__chip" aria-hidden="true">nth-child vertical offsets · aspect-ratio 3/4 portraits · scrim overlay · diagonal --i cascade</p>
</div>
</section>.tr-25,
.tr-25 *,
.tr-25 *::before,
.tr-25 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.tr-25 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
--tr-25-bg: oklch(0.955 0.008 60);
--tr-25-ink: oklch(0.2 0.014 55);
--tr-25-mut: oklch(0.52 0.014 55);
--tr-25-acc: oklch(0.6 0.16 35);
background: var(--tr-25-bg);
color: var(--tr-25-ink);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
-webkit-font-smoothing: antialiased;
}
.tr-25__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: clamp(22px,3.4vw,36px);
padding: clamp(20px,4vw,58px);
}
.tr-25__head {
text-align: center;
display: grid;
gap: 9px;
}
.tr-25__eyebrow {
font-size: 11.5px;
font-weight: 700;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--tr-25-acc);
}
.tr-25__title {
font-size: clamp(24px,4vw,42px);
font-weight: 670;
letter-spacing: -.04em;
line-height: 1.08;
text-wrap: balance;
}
.tr-25__grid {
display: grid;
grid-template-columns: repeat(4,1fr);
gap: clamp(10px,1.5vw,18px);
width: min(100%,1160px);
padding-block-end: 52px;
}
.tr-25__item {
position: relative;
border-radius: 16px;
overflow: hidden;
background: linear-gradient(150deg,oklch(0.85 0.03 50),oklch(0.72 0.05 30));
animation: tr-25-in .8s cubic-bezier(.16,1,.3,1) both;
animation-delay: calc(var(--tr-25-i,0)*70ms);
cursor: pointer;
}
.tr-25__item:nth-child(even) {
margin-block-start: 46px;
}
.tr-25__item:nth-child(4n+3) {
margin-block-start: 24px;
}
@keyframes tr-25-in {
from {
opacity: 0;
translate: 0 22px;
scale: .97;
}
to {
opacity: 1;
translate: 0 0;
scale: 1;
}
}
.tr-25__item img {
width: 100%;
height: auto;
aspect-ratio: 3/4;
object-fit: cover;
display: block;
transition: scale .7s cubic-bezier(.16,1,.3,1),filter .5s;
}
.tr-25__item:hover img,
.tr-25__item:focus-visible img {
scale: 1.06;
}
.tr-25__item:focus-visible {
outline: 3px solid var(--tr-25-acc);
outline-offset: 3px;
}
.tr-25__cap {
position: absolute;
inset-inline: 0;
inset-block-end: 0;
display: grid;
gap: 9px;
padding: clamp(14px,1.6vw,20px);
color: oklch(0.99 0 0);
background: linear-gradient(transparent,oklch(0.14 0.02 40/.5) 34%,oklch(0.12 0.02 40/.92));
translate: 0 12px;
opacity: .86;
transition: translate .45s cubic-bezier(.16,1,.3,1),opacity .45s;
}
.tr-25__item:hover .tr-25__cap,
.tr-25__item:focus-within .tr-25__cap {
translate: 0 0;
opacity: 1;
}
.tr-25__cap blockquote {
font-size: clamp(13px,1.15vw,15px);
line-height: 1.42;
letter-spacing: -.014em;
font-weight: 550;
text-wrap: pretty;
}
.tr-25__cap span {
display: grid;
font-size: 10.5px;
line-height: 1.3;
color: oklch(0.86 0.01 50);
padding-top: 9px;
border-top: 1px solid oklch(1 0 0/.22);
}
.tr-25__cap b {
font-size: 12.5px;
font-weight: 660;
color: oklch(0.99 0 0);
letter-spacing: -.01em;
}
.tr-25__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--tr-25-mut);
padding: 8px 15px;
border: 1px solid oklch(0 0 0/.12);
border-radius: 99px;
text-align: center;
background: oklch(1 0 0/.5);
}
@media (max-width: 960px) {
.tr-25__grid {
grid-template-columns: repeat(3,1fr);
}
.tr-25__item:nth-child(4n+3) {
margin-block-start: 0;
}
.tr-25__item:nth-child(even) {
margin-block-start: 34px;
}
}
@media (max-width: 620px) {
.tr-25__grid {
grid-template-columns: repeat(2,1fr);
padding-block-end: 0;
}
.tr-25__item:nth-child(even),
.tr-25__item:nth-child(4n+3) {
margin-block-start: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.tr-25__item {
animation: none;
}
.tr-25__item img,
.tr-25__cap {
transition: none;
}
}.tr-25,
.tr-25 *,
.tr-25 *::before,
.tr-25 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.tr-25 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
--tr-25-bg: oklch(0.955 0.008 60);
--tr-25-ink: oklch(0.2 0.014 55);
--tr-25-mut: oklch(0.52 0.014 55);
--tr-25-acc: oklch(0.6 0.16 35);
background: var(--tr-25-bg);
color: var(--tr-25-ink);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
-webkit-font-smoothing: antialiased;
}
.tr-25__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: clamp(22px,3.4vw,36px);
padding: clamp(20px,4vw,58px);
}
.tr-25__head {
text-align: center;
display: grid;
gap: 9px;
}
.tr-25__eyebrow {
font-size: 11.5px;
font-weight: 700;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--tr-25-acc);
}
.tr-25__title {
font-size: clamp(24px,4vw,42px);
font-weight: 670;
letter-spacing: -.04em;
line-height: 1.08;
text-wrap: balance;
}
.tr-25__grid {
display: grid;
grid-template-columns: repeat(4,1fr);
gap: clamp(10px,1.5vw,18px);
width: min(100%,1160px);
padding-block-end: 52px;
}
.tr-25__item {
position: relative;
border-radius: 16px;
overflow: hidden;
background: linear-gradient(150deg,oklch(0.85 0.03 50),oklch(0.72 0.05 30));
animation: tr-25-in .8s cubic-bezier(.16,1,.3,1) both;
animation-delay: calc(var(--tr-25-i,0)*70ms);
cursor: pointer;
}
.tr-25__item:nth-child(even) {
margin-block-start: 46px;
}
.tr-25__item:nth-child(4n+3) {
margin-block-start: 24px;
}
@keyframes tr-25-in {
from {
opacity: 0;
translate: 0 22px;
scale: .97;
}
to {
opacity: 1;
translate: 0 0;
scale: 1;
}
}
.tr-25__item img {
width: 100%;
height: auto;
aspect-ratio: 3/4;
object-fit: cover;
display: block;
transition: scale .7s cubic-bezier(.16,1,.3,1),filter .5s;
}
.tr-25__item:hover img,
.tr-25__item:focus-visible img {
scale: 1.06;
}
.tr-25__item:focus-visible {
outline: 3px solid var(--tr-25-acc);
outline-offset: 3px;
}
.tr-25__cap {
position: absolute;
inset-inline: 0;
inset-block-end: 0;
display: grid;
gap: 9px;
padding: clamp(14px,1.6vw,20px);
color: oklch(0.99 0 0);
background: linear-gradient(transparent,oklch(0.14 0.02 40/.5) 34%,oklch(0.12 0.02 40/.92));
translate: 0 12px;
opacity: .86;
transition: translate .45s cubic-bezier(.16,1,.3,1),opacity .45s;
}
.tr-25__item:hover .tr-25__cap,
.tr-25__item:focus-within .tr-25__cap {
translate: 0 0;
opacity: 1;
}
.tr-25__cap blockquote {
font-size: clamp(13px,1.15vw,15px);
line-height: 1.42;
letter-spacing: -.014em;
font-weight: 550;
text-wrap: pretty;
}
.tr-25__cap span {
display: grid;
font-size: 10.5px;
line-height: 1.3;
color: oklch(0.86 0.01 50);
padding-top: 9px;
border-top: 1px solid oklch(1 0 0/.22);
}
.tr-25__cap b {
font-size: 12.5px;
font-weight: 660;
color: oklch(0.99 0 0);
letter-spacing: -.01em;
}
.tr-25__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--tr-25-mut);
padding: 8px 15px;
border: 1px solid oklch(0 0 0/.12);
border-radius: 99px;
text-align: center;
background: oklch(1 0 0/.5);
}
@media (max-width: 960px) {
.tr-25__grid {
grid-template-columns: repeat(3,1fr);
}
.tr-25__item:nth-child(4n+3) {
margin-block-start: 0;
}
.tr-25__item:nth-child(even) {
margin-block-start: 34px;
}
}
@media (max-width: 620px) {
.tr-25__grid {
grid-template-columns: repeat(2,1fr);
padding-block-end: 0;
}
.tr-25__item:nth-child(even),
.tr-25__item:nth-child(4n+3) {
margin-block-start: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.tr-25__item {
animation: none;
}
.tr-25__item img,
.tr-25__cap {
transition: none;
}
}Here's a working CSS Testimonials & Review from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Staggered Portrait Testimonial Grid
Source: https://codefronts.com/components/css-testimonials-reviews/portrait-grid/
Faces first. When the endorsement is the person — founders, influencers, practitioners — this staggered portrait grid outperforms any card layout. Alternate columns are offset vertically with an nth-child rule, each portrait carries its quote in an overlay that lifts on hover, and the whole grid reveals in a diagonal cascade.
## HTML
```html
<section class="tr-25" aria-label="Staggered portrait testimonial grid demo">
<div class="tr-25__stage">
<header class="tr-25__head">
<p class="tr-25__eyebrow">The people, not the logos</p>
<h2 class="tr-25__title">Eight founders. One sentence each.</h2>
</header>
<div class="tr-25__grid">
<figure class="tr-25__item" style="--tr-25-i:0" tabindex="0">
<img src="https://images.unsplash.com/photo-1573497019940-1c28c88b4f3e?q=80&w=600&auto=format&fit=crop" alt="Portrait of Alessandra Rossi" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Nineteen years of records moved in a weekend.”</blockquote><span><b>Alessandra Rossi</b>Vaultline Financial</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:1" tabindex="0">
<img src="https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?q=80&w=600&auto=format&fit=crop" alt="Portrait of Marcus Yeung" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Compliance stopped filing tickets and started filing praise.”</blockquote><span><b>Marcus Yeung</b>Halden Group</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:2" tabindex="0">
<img src="https://images.unsplash.com/photo-1517841905240-472988babdf9?q=80&w=600&auto=format&fit=crop" alt="Portrait of Grace Whitlock" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“94% of merged PRs now reference a token.”</blockquote><span><b>Grace Whitlock</b>Auryn</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:3" tabindex="0">
<img src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=600&auto=format&fit=crop" alt="Portrait of Dan Reyes" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Two data centres, four time zones, zero rollbacks.”</blockquote><span><b>Dan Reyes</b>Portside</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:4" tabindex="0">
<img src="https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=600&auto=format&fit=crop" alt="Portrait of Hana Sørensen" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Asked on Friday. Shipped Tuesday. With docs.”</blockquote><span><b>Hana Sørensen</b>Blomst</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:5" tabindex="0">
<img src="https://images.unsplash.com/photo-1534528741775-53994a69daeb?q=80&w=600&auto=format&fit=crop" alt="Portrait of Nia Osei" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Onboarding took an afternoon, not a quarter.”</blockquote><span><b>Nia Osei</b>Fieldwork</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:6" tabindex="0">
<img src="https://images.unsplash.com/photo-1580489944761-15a19d654956?q=80&w=600&auto=format&fit=crop" alt="Portrait of Erin McAllister" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Renewal was a fifteen-minute conversation.”</blockquote><span><b>Erin McAllister</b>Larkfield Health</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:7" tabindex="0">
<img src="https://images.unsplash.com/photo-1463453091185-61582044d556?q=80&w=600&auto=format&fit=crop" alt="Portrait of Samuel Idris" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“The first build took nine seconds. I ran it twice.”</blockquote><span><b>Samuel Idris</b>Ashcombe Labs</span></figcaption>
</figure>
</div>
<p class="tr-25__chip" aria-hidden="true">nth-child vertical offsets · aspect-ratio 3/4 portraits · scrim overlay · diagonal --i cascade</p>
</div>
</section>
```
## CSS
```css
.tr-25,
.tr-25 *,
.tr-25 *::before,
.tr-25 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.tr-25 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
--tr-25-bg: oklch(0.955 0.008 60);
--tr-25-ink: oklch(0.2 0.014 55);
--tr-25-mut: oklch(0.52 0.014 55);
--tr-25-acc: oklch(0.6 0.16 35);
background: var(--tr-25-bg);
color: var(--tr-25-ink);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
-webkit-font-smoothing: antialiased;
}
.tr-25__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: clamp(22px,3.4vw,36px);
padding: clamp(20px,4vw,58px);
}
.tr-25__head {
text-align: center;
display: grid;
gap: 9px;
}
.tr-25__eyebrow {
font-size: 11.5px;
font-weight: 700;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--tr-25-acc);
}
.tr-25__title {
font-size: clamp(24px,4vw,42px);
font-weight: 670;
letter-spacing: -.04em;
line-height: 1.08;
text-wrap: balance;
}
.tr-25__grid {
display: grid;
grid-template-columns: repeat(4,1fr);
gap: clamp(10px,1.5vw,18px);
width: min(100%,1160px);
padding-block-end: 52px;
}
.tr-25__item {
position: relative;
border-radius: 16px;
overflow: hidden;
background: linear-gradient(150deg,oklch(0.85 0.03 50),oklch(0.72 0.05 30));
animation: tr-25-in .8s cubic-bezier(.16,1,.3,1) both;
animation-delay: calc(var(--tr-25-i,0)*70ms);
cursor: pointer;
}
.tr-25__item:nth-child(even) {
margin-block-start: 46px;
}
.tr-25__item:nth-child(4n+3) {
margin-block-start: 24px;
}
@keyframes tr-25-in {
from {
opacity: 0;
translate: 0 22px;
scale: .97;
}
to {
opacity: 1;
translate: 0 0;
scale: 1;
}
}
.tr-25__item img {
width: 100%;
height: auto;
aspect-ratio: 3/4;
object-fit: cover;
display: block;
transition: scale .7s cubic-bezier(.16,1,.3,1),filter .5s;
}
.tr-25__item:hover img,
.tr-25__item:focus-visible img {
scale: 1.06;
}
.tr-25__item:focus-visible {
outline: 3px solid var(--tr-25-acc);
outline-offset: 3px;
}
.tr-25__cap {
position: absolute;
inset-inline: 0;
inset-block-end: 0;
display: grid;
gap: 9px;
padding: clamp(14px,1.6vw,20px);
color: oklch(0.99 0 0);
background: linear-gradient(transparent,oklch(0.14 0.02 40/.5) 34%,oklch(0.12 0.02 40/.92));
translate: 0 12px;
opacity: .86;
transition: translate .45s cubic-bezier(.16,1,.3,1),opacity .45s;
}
.tr-25__item:hover .tr-25__cap,
.tr-25__item:focus-within .tr-25__cap {
translate: 0 0;
opacity: 1;
}
.tr-25__cap blockquote {
font-size: clamp(13px,1.15vw,15px);
line-height: 1.42;
letter-spacing: -.014em;
font-weight: 550;
text-wrap: pretty;
}
.tr-25__cap span {
display: grid;
font-size: 10.5px;
line-height: 1.3;
color: oklch(0.86 0.01 50);
padding-top: 9px;
border-top: 1px solid oklch(1 0 0/.22);
}
.tr-25__cap b {
font-size: 12.5px;
font-weight: 660;
color: oklch(0.99 0 0);
letter-spacing: -.01em;
}
.tr-25__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--tr-25-mut);
padding: 8px 15px;
border: 1px solid oklch(0 0 0/.12);
border-radius: 99px;
text-align: center;
background: oklch(1 0 0/.5);
}
@media (max-width: 960px) {
.tr-25__grid {
grid-template-columns: repeat(3,1fr);
}
.tr-25__item:nth-child(4n+3) {
margin-block-start: 0;
}
.tr-25__item:nth-child(even) {
margin-block-start: 34px;
}
}
@media (max-width: 620px) {
.tr-25__grid {
grid-template-columns: repeat(2,1fr);
padding-block-end: 0;
}
.tr-25__item:nth-child(even),
.tr-25__item:nth-child(4n+3) {
margin-block-start: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.tr-25__item {
animation: none;
}
.tr-25__item img,
.tr-25__cap {
transition: none;
}
}
```Here's a working CSS Testimonials & Review from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Staggered Portrait Testimonial Grid
Source: https://codefronts.com/components/css-testimonials-reviews/portrait-grid/
Faces first. When the endorsement is the person — founders, influencers, practitioners — this staggered portrait grid outperforms any card layout. Alternate columns are offset vertically with an nth-child rule, each portrait carries its quote in an overlay that lifts on hover, and the whole grid reveals in a diagonal cascade.
## HTML
```html
<section class="tr-25" aria-label="Staggered portrait testimonial grid demo">
<div class="tr-25__stage">
<header class="tr-25__head">
<p class="tr-25__eyebrow">The people, not the logos</p>
<h2 class="tr-25__title">Eight founders. One sentence each.</h2>
</header>
<div class="tr-25__grid">
<figure class="tr-25__item" style="--tr-25-i:0" tabindex="0">
<img src="https://images.unsplash.com/photo-1573497019940-1c28c88b4f3e?q=80&w=600&auto=format&fit=crop" alt="Portrait of Alessandra Rossi" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Nineteen years of records moved in a weekend.”</blockquote><span><b>Alessandra Rossi</b>Vaultline Financial</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:1" tabindex="0">
<img src="https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?q=80&w=600&auto=format&fit=crop" alt="Portrait of Marcus Yeung" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Compliance stopped filing tickets and started filing praise.”</blockquote><span><b>Marcus Yeung</b>Halden Group</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:2" tabindex="0">
<img src="https://images.unsplash.com/photo-1517841905240-472988babdf9?q=80&w=600&auto=format&fit=crop" alt="Portrait of Grace Whitlock" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“94% of merged PRs now reference a token.”</blockquote><span><b>Grace Whitlock</b>Auryn</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:3" tabindex="0">
<img src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=600&auto=format&fit=crop" alt="Portrait of Dan Reyes" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Two data centres, four time zones, zero rollbacks.”</blockquote><span><b>Dan Reyes</b>Portside</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:4" tabindex="0">
<img src="https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=600&auto=format&fit=crop" alt="Portrait of Hana Sørensen" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Asked on Friday. Shipped Tuesday. With docs.”</blockquote><span><b>Hana Sørensen</b>Blomst</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:5" tabindex="0">
<img src="https://images.unsplash.com/photo-1534528741775-53994a69daeb?q=80&w=600&auto=format&fit=crop" alt="Portrait of Nia Osei" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Onboarding took an afternoon, not a quarter.”</blockquote><span><b>Nia Osei</b>Fieldwork</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:6" tabindex="0">
<img src="https://images.unsplash.com/photo-1580489944761-15a19d654956?q=80&w=600&auto=format&fit=crop" alt="Portrait of Erin McAllister" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“Renewal was a fifteen-minute conversation.”</blockquote><span><b>Erin McAllister</b>Larkfield Health</span></figcaption>
</figure>
<figure class="tr-25__item" style="--tr-25-i:7" tabindex="0">
<img src="https://images.unsplash.com/photo-1463453091185-61582044d556?q=80&w=600&auto=format&fit=crop" alt="Portrait of Samuel Idris" width="440" height="586" loading="lazy" decoding="async">
<figcaption class="tr-25__cap"><blockquote>“The first build took nine seconds. I ran it twice.”</blockquote><span><b>Samuel Idris</b>Ashcombe Labs</span></figcaption>
</figure>
</div>
<p class="tr-25__chip" aria-hidden="true">nth-child vertical offsets · aspect-ratio 3/4 portraits · scrim overlay · diagonal --i cascade</p>
</div>
</section>
```
## CSS
```css
.tr-25,
.tr-25 *,
.tr-25 *::before,
.tr-25 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.tr-25 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
--tr-25-bg: oklch(0.955 0.008 60);
--tr-25-ink: oklch(0.2 0.014 55);
--tr-25-mut: oklch(0.52 0.014 55);
--tr-25-acc: oklch(0.6 0.16 35);
background: var(--tr-25-bg);
color: var(--tr-25-ink);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
-webkit-font-smoothing: antialiased;
}
.tr-25__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: clamp(22px,3.4vw,36px);
padding: clamp(20px,4vw,58px);
}
.tr-25__head {
text-align: center;
display: grid;
gap: 9px;
}
.tr-25__eyebrow {
font-size: 11.5px;
font-weight: 700;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--tr-25-acc);
}
.tr-25__title {
font-size: clamp(24px,4vw,42px);
font-weight: 670;
letter-spacing: -.04em;
line-height: 1.08;
text-wrap: balance;
}
.tr-25__grid {
display: grid;
grid-template-columns: repeat(4,1fr);
gap: clamp(10px,1.5vw,18px);
width: min(100%,1160px);
padding-block-end: 52px;
}
.tr-25__item {
position: relative;
border-radius: 16px;
overflow: hidden;
background: linear-gradient(150deg,oklch(0.85 0.03 50),oklch(0.72 0.05 30));
animation: tr-25-in .8s cubic-bezier(.16,1,.3,1) both;
animation-delay: calc(var(--tr-25-i,0)*70ms);
cursor: pointer;
}
.tr-25__item:nth-child(even) {
margin-block-start: 46px;
}
.tr-25__item:nth-child(4n+3) {
margin-block-start: 24px;
}
@keyframes tr-25-in {
from {
opacity: 0;
translate: 0 22px;
scale: .97;
}
to {
opacity: 1;
translate: 0 0;
scale: 1;
}
}
.tr-25__item img {
width: 100%;
height: auto;
aspect-ratio: 3/4;
object-fit: cover;
display: block;
transition: scale .7s cubic-bezier(.16,1,.3,1),filter .5s;
}
.tr-25__item:hover img,
.tr-25__item:focus-visible img {
scale: 1.06;
}
.tr-25__item:focus-visible {
outline: 3px solid var(--tr-25-acc);
outline-offset: 3px;
}
.tr-25__cap {
position: absolute;
inset-inline: 0;
inset-block-end: 0;
display: grid;
gap: 9px;
padding: clamp(14px,1.6vw,20px);
color: oklch(0.99 0 0);
background: linear-gradient(transparent,oklch(0.14 0.02 40/.5) 34%,oklch(0.12 0.02 40/.92));
translate: 0 12px;
opacity: .86;
transition: translate .45s cubic-bezier(.16,1,.3,1),opacity .45s;
}
.tr-25__item:hover .tr-25__cap,
.tr-25__item:focus-within .tr-25__cap {
translate: 0 0;
opacity: 1;
}
.tr-25__cap blockquote {
font-size: clamp(13px,1.15vw,15px);
line-height: 1.42;
letter-spacing: -.014em;
font-weight: 550;
text-wrap: pretty;
}
.tr-25__cap span {
display: grid;
font-size: 10.5px;
line-height: 1.3;
color: oklch(0.86 0.01 50);
padding-top: 9px;
border-top: 1px solid oklch(1 0 0/.22);
}
.tr-25__cap b {
font-size: 12.5px;
font-weight: 660;
color: oklch(0.99 0 0);
letter-spacing: -.01em;
}
.tr-25__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--tr-25-mut);
padding: 8px 15px;
border: 1px solid oklch(0 0 0/.12);
border-radius: 99px;
text-align: center;
background: oklch(1 0 0/.5);
}
@media (max-width: 960px) {
.tr-25__grid {
grid-template-columns: repeat(3,1fr);
}
.tr-25__item:nth-child(4n+3) {
margin-block-start: 0;
}
.tr-25__item:nth-child(even) {
margin-block-start: 34px;
}
}
@media (max-width: 620px) {
.tr-25__grid {
grid-template-columns: repeat(2,1fr);
padding-block-end: 0;
}
.tr-25__item:nth-child(even),
.tr-25__item:nth-child(4n+3) {
margin-block-start: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.tr-25__item {
animation: none;
}
.tr-25__item img,
.tr-25__cap {
transition: none;
}
}
```How this works
The stagger is one rule, not a hand-built layout:
.grid { display: grid;
grid-template-columns: repeat(4, 1fr); gap: 16px; }
.item:nth-child(even) { margin-block-start: 44px; }
.item:nth-child(4n + 3) { margin-block-start: 22px; }Two nth-child selectors give three different vertical positions across the row, which is enough irregularity to read as designed rather than as a bug. Everything else stays on the grid, so the columns are still perfectly aligned horizontally.
Each tile is a <figure> with the portrait and an overlay caption. The overlay sits on a gradient scrim that is always partially visible — never fully hidden until hover, because a quote that only exists on hover does not exist on touch:
.cap { translate: 0 12px; opacity: .82;
transition: translate .45s cubic-bezier(.16,1,.3,1), opacity .45s; }
.item:hover .cap, .item:focus-within .cap { translate: 0 0; opacity: 1; }The diagonal reveal uses a per-item --i index in the delay calculation, so adding a tile needs no new CSS. Portraits are aspect-ratio: 3/4 with object-fit: cover, which reserves the box before the image lands and keeps the stagger stable during loading.
On narrow screens the offsets are removed entirely — a staggered two-column grid on a phone just looks broken, and the single-column fallback is the honest answer.
Make it yours
- Change the rhythm by editing the two nth-child offsets;
:nth-child(3n)against a 4-column grid gives a longer, less regular repeat. - Add a subtle per-tile duotone with
filter: grayscale(1)clearing on hover for a more editorial, less lifestyle feel. - Make each tile a link to the full case study — wrap the figure in an
<a>and move the hover styles onto:hover, :focus-visibleof the anchor. - For six tiles instead of eight, switch to 3 columns and offset only
:nth-child(even)— the stagger reads best when roughly half the tiles move.
Gotchas — read before shipping
- Vertical offsets change where a grid row ends, so the section's bottom edge becomes ragged. Add matching bottom padding or the next section will look misaligned.
- Text over photography needs a scrim with real opacity, not just a text-shadow. Test the worst-case image, which is always a bright one.
- Hover-only captions are invisible on touch devices. Keep the quote at least 80% visible at rest, as here.
aspect-ratioon a flex or grid child can be overridden by stretch alignment. Setalign-self: startif the ratio is being ignored.- Portraits of real people require permission for marketing use in every Tier-1 jurisdiction — and a signed release if the endorsement implies a paid relationship.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by oklch(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 88+.
aspect-ratio is Chrome 88, Safari 15, Firefox 89. Grid, nth-child offsets and transitions are universal. oklch() raises the floor to 2023 engines and is trivially replaceable with hex.