SectionsSimpleApril 21, 2026
Testimonial Grid
A 3-column testimonial card grid with star ratings, italic serif quotes, and attribution lines. Warm cream background with white cards. Shopify CMS-ready with commented integration code for metaobjects and review apps.
View Full Demo →Preview
What Customers Say
Don't take our word for it.
★★★★★
“I have really dry skin on the back of my arms and haven't found anything that locks in hydration like this. After four uses, I'm already seeing a difference.”
Alura M. · Vanilla Body Butter
★★★★★
“Started using this in my third trimester and my stretch marks are noticeably softer. I put it on my daughter's eczema patches too — the only thing that doesn't make her fuss.”
Priya T. · Unscented Body Butter
★★★★★
“Bought the mini set as baby shower favors. Every single person asked me where to get more. Now I just buy them in bulk.”
Danielle R. · Mini Set of Three
Source
demo.jsx
import TestimonialGrid from "./index.jsx";
import { testimonialGrid } from "../demo-data.js";
export default function TestimonialGridDemo() {
return <TestimonialGrid {...testimonialGrid} />;
}
index.jsx
"use client";
import styles from "./styles.module.css";
/*
── Shopify CMS Integration ──
To connect to Shopify metaobjects or a reviews app:
// Option A: Shopify Metaobjects (Admin API)
// Create a metaobject definition "testimonial" with fields:
// - quote (multi_line_text_field)
// - name (single_line_text_field)
// - product (single_line_text_field)
// - rating (number_integer, min: 1, max: 5)
//
// Query via Storefront API:
// const TESTIMONIALS_QUERY = `
// query {
// metaobjects(type: "testimonial", first: 6) {
// nodes {
// fields {
// key
// value
// }
// }
// }
// }
// `;
//
// Then map the response:
// const items = data.metaobjects.nodes.map(node => ({
// quote: node.fields.find(f => f.key === "quote")?.value,
// name: node.fields.find(f => f.key === "name")?.value,
// product: node.fields.find(f => f.key === "product")?.value,
// rating: parseInt(node.fields.find(f => f.key === "rating")?.value) || 5,
// }));
// Option B: Judge.me / Stamped / Loox
// Fetch from their public API endpoint:
// const res = await fetch(`https://judge.me/api/v1/reviews?shop_domain=${shop}&api_token=${token}`);
// Map response to same { quote, name, product, rating } shape.
*/
function Stars({ count = 5 }) {
return (
<div className={styles.stars}>
{Array.from({ length: count }).map((_, i) => (
<span key={i} className={styles.star}>★</span>
))}
</div>
);
}
export default function TestimonialGrid({
label = "What Customers Say",
heading,
headingEmphasis,
items = [],
}) {
// Build heading with optional italic emphasis word
const renderHeading = () => {
if (!headingEmphasis || !heading) return heading;
const parts = heading.split(headingEmphasis);
if (parts.length < 2) return heading;
return (
<>
{parts[0]}<em>{headingEmphasis}</em>{parts[1]}
</>
);
};
return (
<section className={styles.section}>
{label && <p className={styles.label}>{label}</p>}
{heading && <h2 className={styles.heading}>{renderHeading()}</h2>}
<div className={styles.grid}>
{items.map((item, i) => (
<div key={i} className={styles.card}>
<Stars count={item.rating || 5} />
<blockquote className={styles.quote}>
“{item.quote}”
</blockquote>
<p className={styles.attribution}>
{item.name}
{item.product && (
<>
<span className={styles.dot}> · </span>
{item.product}
</>
)}
</p>
</div>
))}
</div>
</section>
);
}
styles.module.css
.section {
width: 100%;
background: #f1ebe7;
padding: 6rem 2rem 8rem;
text-align: center;
}
/* ---- Label ---- */
.label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.2em;
color: #555;
margin-bottom: 1.5rem;
}
/* ---- Heading ---- */
.heading {
font-size: clamp(1.75rem, 3.5vw, 3rem);
font-weight: 400;
line-height: 1.2;
color: #1a1a1a;
margin-bottom: 3.5rem;
font-family: Georgia, "Times New Roman", serif;
}
.heading em {
font-style: italic;
}
/* ---- Grid ---- */
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
max-width: 1200px;
margin: 0 auto;
}
/* ---- Card ---- */
.card {
background: #fff;
border-radius: 0.5rem;
padding: 2.5rem 2rem 2rem;
text-align: left;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
/* ---- Stars ---- */
.stars {
display: flex;
gap: 0.15rem;
}
.star {
color: #a8875b;
font-size: 0.9rem;
}
/* ---- Quote ---- */
.quote {
font-family: Georgia, "Times New Roman", serif;
font-style: italic;
font-size: clamp(1rem, 1.3vw, 1.2rem);
line-height: 1.55;
color: #1a1a1a;
flex: 1;
}
/* ---- Attribution ---- */
.attribution {
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: #555;
margin-top: auto;
}
.dot {
color: #999;
}
/* ---- Desktop ---- */
@media (min-width: 768px) {
.grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* ---- Mobile ---- */
@media (max-width: 767px) {
.section {
padding: 4rem 1rem 5rem;
}
.card {
padding: 2rem 1.5rem 1.5rem;
}
}