Construction — Customer Reviews
A rugged customer reviews section for a construction firm with an aggregate rating header, per-star breakdown bars, verified-badge review cards tagged by project type, and live filter-by-rating plus sort-by-recent or highest controls driven entirely by vanilla JavaScript.
MCP
Code
:root {
--hv: #f5c518;
--steel: #2b3440;
--steel-d: #1b222b;
--bg: #eceae6;
--ink: #1a1d22;
--muted: #666c75;
--orange: #e8642a;
--white: #ffffff;
--border: #d7d4cd;
--radius: 10px;
--shadow: 0 1px 0 rgba(27, 34, 43, 0.04), 0 6px 18px rgba(27, 34, 43, 0.08);
--font: 'Inter', system-ui, -apple-system, sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
}
body {
font-family: var(--font);
background: var(--bg);
color: var(--ink);
line-height: 1.5;
padding: 32px 16px;
-webkit-font-smoothing: antialiased;
}
.reviews {
max-width: 760px;
margin: 0 auto;
}
/* ---- aggregate header ---- */
.reviews__head {
display: grid;
grid-template-columns: auto 1fr;
gap: 28px;
align-items: center;
background: var(--steel);
color: var(--white);
border-radius: var(--radius);
padding: 26px 28px;
box-shadow: var(--shadow);
position: relative;
overflow: hidden;
}
.agg__hazard {
position: absolute;
inset: 0 auto 0 0;
width: 6px;
background: repeating-linear-gradient(
-45deg,
var(--hv) 0 10px,
var(--steel-d) 10px 20px
);
}
.agg__score {
padding-left: 12px;
}
.agg__num {
display: block;
font-size: 52px;
font-weight: 800;
line-height: 1;
letter-spacing: -0.02em;
}
.agg__count {
font-size: 13px;
color: #c7cdd4;
margin-top: 6px;
}
.agg__count strong {
color: var(--white);
}
/* star glyph fill */
.stars {
--fill: 100%;
display: inline-block;
position: relative;
font-size: 18px;
line-height: 1;
letter-spacing: 2px;
color: rgba(255, 255, 255, 0.22);
white-space: nowrap;
}
.stars::before {
content: '★★★★★';
position: absolute;
inset: 0;
width: var(--fill);
overflow: hidden;
color: var(--hv);
}
.agg__stars .stars {
font-size: 22px;
margin: 6px 0 2px;
}
/* breakdown bars */
.breakdown {
display: grid;
gap: 7px;
}
.bar-row {
display: grid;
grid-template-columns: 28px 1fr 38px;
align-items: center;
gap: 10px;
font-size: 12px;
font-weight: 600;
color: #c7cdd4;
}
.bar {
height: 8px;
background: rgba(255, 255, 255, 0.12);
border-radius: 99px;
overflow: hidden;
}
.bar__fill {
display: block;
height: 100%;
background: var(--hv);
border-radius: 99px;
}
.bar-row__pct {
text-align: right;
}
/* ---- title ---- */
.reviews__title {
font-size: 26px;
font-weight: 800;
letter-spacing: -0.02em;
text-transform: uppercase;
margin: 30px 0 14px;
}
.reviews__title::after {
content: '';
display: block;
width: 56px;
height: 5px;
margin-top: 8px;
background: var(--orange);
border-radius: 2px;
}
/* ---- toolbar ---- */
.toolbar {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
justify-content: space-between;
gap: 14px;
margin-bottom: 18px;
}
.toolbar__group {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.field {
display: grid;
gap: 5px;
}
.field__label {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
}
.select {
font: inherit;
font-weight: 600;
color: var(--ink);
background: var(--white);
border: 2px solid var(--steel);
border-radius: 8px;
padding: 9px 12px;
cursor: pointer;
min-width: 150px;
}
.select:focus-visible {
outline: 3px solid var(--hv);
outline-offset: 1px;
}
.toolbar__count {
font-size: 13px;
font-weight: 600;
color: var(--muted);
}
/* ---- review cards ---- */
.cards {
list-style: none;
padding: 0;
display: grid;
gap: 14px;
}
.card {
background: var(--white);
border: 1px solid var(--border);
border-left: 5px solid var(--steel);
border-radius: var(--radius);
padding: 18px 20px;
box-shadow: var(--shadow);
transition: transform 0.15s ease, border-left-color 0.15s ease;
}
.card:hover {
transform: translateY(-2px);
border-left-color: var(--orange);
}
.card__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.card__who {
display: flex;
gap: 12px;
align-items: center;
}
.avatar {
flex: none;
width: 42px;
height: 42px;
display: grid;
place-items: center;
border-radius: 8px;
background: var(--steel);
color: var(--hv);
font-weight: 800;
font-size: 14px;
letter-spacing: 0.02em;
}
.card__name {
font-weight: 700;
font-size: 15px;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.verified {
font-size: 10.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #1f7a3d;
background: #e2f3e8;
border: 1px solid #bfe4cb;
padding: 2px 7px;
border-radius: 99px;
}
.card__project {
font-size: 12.5px;
color: var(--muted);
margin-top: 1px;
}
.card__top .stars {
color: #e4e1da;
flex: none;
}
.card__text {
font-size: 14.5px;
color: #34393f;
margin: 12px 0 10px;
}
.card__date {
font-size: 12px;
font-weight: 600;
color: var(--muted);
}
.empty {
text-align: center;
padding: 36px 16px;
color: var(--muted);
font-weight: 600;
border: 2px dashed var(--border);
border-radius: var(--radius);
}
/* ---- responsive ---- */
@media (max-width: 520px) {
body {
padding: 20px 12px;
}
.reviews__head {
grid-template-columns: 1fr;
gap: 20px;
padding: 22px;
}
.agg__num {
font-size: 44px;
}
.toolbar {
flex-direction: column;
align-items: stretch;
}
.toolbar__group {
flex-direction: column;
}
.select {
width: 100%;
}
.card__top {
flex-direction: column;
}
.card__top .stars {
align-self: flex-start;
}
}(function () {
'use strict';
var list = document.getElementById('cards');
var filterEl = document.getElementById('filter');
var sortEl = document.getElementById('sort');
var countEl = document.getElementById('count');
var emptyEl = document.getElementById('empty');
if (!list || !filterEl || !sortEl) return;
// Snapshot the original cards so we can re-order and re-filter from a stable source.
var cards = Array.prototype.slice.call(list.querySelectorAll('.card'));
function rating(card) {
return parseInt(card.getAttribute('data-rating'), 10) || 0;
}
function dateValue(card) {
return Date.parse(card.getAttribute('data-date')) || 0;
}
function render() {
var filter = filterEl.value; // 'all' or '1'..'5'
var sort = sortEl.value; // 'recent' or 'highest'
var visible = cards.filter(function (card) {
return filter === 'all' || String(rating(card)) === filter;
});
visible.sort(function (a, b) {
if (sort === 'highest') {
// Highest rating first, then newest as a tiebreaker.
return rating(b) - rating(a) || dateValue(b) - dateValue(a);
}
return dateValue(b) - dateValue(a); // most recent first
});
// Hide everything, then append the visible set in order.
cards.forEach(function (card) {
card.hidden = true;
});
visible.forEach(function (card) {
card.hidden = false;
list.appendChild(card);
});
var n = visible.length;
countEl.textContent = 'Showing ' + n + (n === 1 ? ' review' : ' reviews');
list.hidden = n === 0;
emptyEl.hidden = n !== 0;
}
filterEl.addEventListener('change', render);
sortEl.addEventListener('change', render);
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Customer Reviews — Ironclad Builders</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="reviews" aria-labelledby="reviews-title">
<header class="reviews__head">
<div class="agg">
<p class="agg__hazard" aria-hidden="true"></p>
<div class="agg__score">
<span class="agg__num">4.6</span>
<div class="agg__stars" role="img" aria-label="4.6 out of 5 stars">
<span class="stars" style="--fill: 92%">★★★★★</span>
</div>
<p class="agg__count">Based on <strong>248</strong> verified reviews</p>
</div>
</div>
<div class="breakdown" aria-hidden="true">
<div class="bar-row">
<span class="bar-row__label">5★</span>
<span class="bar"><span class="bar__fill" style="width: 78%"></span></span>
<span class="bar-row__pct">78%</span>
</div>
<div class="bar-row">
<span class="bar-row__label">4★</span>
<span class="bar"><span class="bar__fill" style="width: 14%"></span></span>
<span class="bar-row__pct">14%</span>
</div>
<div class="bar-row">
<span class="bar-row__label">3★</span>
<span class="bar"><span class="bar__fill" style="width: 5%"></span></span>
<span class="bar-row__pct">5%</span>
</div>
<div class="bar-row">
<span class="bar-row__label">2★</span>
<span class="bar"><span class="bar__fill" style="width: 2%"></span></span>
<span class="bar-row__pct">2%</span>
</div>
<div class="bar-row">
<span class="bar-row__label">1★</span>
<span class="bar"><span class="bar__fill" style="width: 1%"></span></span>
<span class="bar-row__pct">1%</span>
</div>
</div>
</header>
<h1 id="reviews-title" class="reviews__title">What our clients say</h1>
<div class="toolbar">
<div class="toolbar__group">
<label class="field">
<span class="field__label">Filter by rating</span>
<select id="filter" class="select">
<option value="all">All ratings</option>
<option value="5">5 stars</option>
<option value="4">4 stars</option>
<option value="3">3 stars</option>
<option value="2">2 stars</option>
<option value="1">1 star</option>
</select>
</label>
<label class="field">
<span class="field__label">Sort by</span>
<select id="sort" class="select">
<option value="recent">Most recent</option>
<option value="highest">Highest rated</option>
</select>
</label>
</div>
<p class="toolbar__count" id="count" aria-live="polite">Showing 6 reviews</p>
</div>
<ul class="cards" id="cards">
<li class="card" data-rating="5" data-date="2026-06-09">
<div class="card__top">
<div class="card__who">
<span class="avatar" aria-hidden="true">MR</span>
<div>
<p class="card__name">
Marcus Reyes
<span class="verified" title="Verified customer">✓ Verified</span>
</p>
<p class="card__project">Roof replacement · Asphalt shingle</p>
</div>
</div>
<span class="stars" role="img" aria-label="5 out of 5 stars" style="--fill: 100%">★★★★★</span>
</div>
<p class="card__text">
Crew tore off and re-roofed our 2-story in a single day. Foreman walked me through every
step and the cleanup was spotless — not a single nail left in the driveway. Quote held
firm with no surprise charges.
</p>
<time class="card__date" datetime="2026-06-09">June 9, 2026</time>
</li>
<li class="card" data-rating="5" data-date="2026-05-22">
<div class="card__top">
<div class="card__who">
<span class="avatar" aria-hidden="true">DK</span>
<div>
<p class="card__name">
Denise Kowalski
<span class="verified" title="Verified customer">✓ Verified</span>
</p>
<p class="card__project">Kitchen remodel · Full gut</p>
</div>
</div>
<span class="stars" role="img" aria-label="5 out of 5 stars" style="--fill: 100%">★★★★★</span>
</div>
<p class="card__text">
Took our 1980s galley down to the studs and rebuilt it open-concept. The cabinet
install and quartz seams are flawless. Project finished two days ahead of the 6-week
schedule.
</p>
<time class="card__date" datetime="2026-05-22">May 22, 2026</time>
</li>
<li class="card" data-rating="4" data-date="2026-04-30">
<div class="card__top">
<div class="card__who">
<span class="avatar" aria-hidden="true">TN</span>
<div>
<p class="card__name">
Trevor Nguyen
<span class="verified" title="Verified customer">✓ Verified</span>
</p>
<p class="card__project">Concrete pour · Driveway</p>
</div>
</div>
<span class="stars" role="img" aria-label="4 out of 5 stars" style="--fill: 80%">★★★★★</span>
</div>
<p class="card__text">
Solid pour and a clean broom finish — the slab looks great six months on with no
cracking. Lost a star only because the start date slipped a week, but they kept me
posted the whole time.
</p>
<time class="card__date" datetime="2026-04-30">April 30, 2026</time>
</li>
<li class="card" data-rating="5" data-date="2026-03-14">
<div class="card__top">
<div class="card__who">
<span class="avatar" aria-hidden="true">AB</span>
<div>
<p class="card__name">
Aisha Bello
<span class="verified" title="Verified customer">✓ Verified</span>
</p>
<p class="card__project">Bathroom remodel · Master ensuite</p>
</div>
</div>
<span class="stars" role="img" aria-label="5 out of 5 stars" style="--fill: 100%">★★★★★</span>
</div>
<p class="card__text">
Beautiful tile work and the curbless shower drains perfectly. They protected the
hallway floors with ram board every day and the plumber caught an old leak we never
knew about. Worth every dollar.
</p>
<time class="card__date" datetime="2026-03-14">March 14, 2026</time>
</li>
<li class="card" data-rating="3" data-date="2026-02-08">
<div class="card__top">
<div class="card__who">
<span class="avatar" aria-hidden="true">JC</span>
<div>
<p class="card__name">
Jordan Castillo
<span class="verified" title="Verified customer">✓ Verified</span>
</p>
<p class="card__project">Deck build · Composite</p>
</div>
</div>
<span class="stars" role="img" aria-label="3 out of 5 stars" style="--fill: 60%">★★★★★</span>
</div>
<p class="card__text">
The finished deck is sturdy and looks sharp, but communication during the build was
spotty and the permit took longer than I was told. Once the crew was on site the
workmanship was good.
</p>
<time class="card__date" datetime="2026-02-08">February 8, 2026</time>
</li>
<li class="card" data-rating="4" data-date="2026-01-19">
<div class="card__top">
<div class="card__who">
<span class="avatar" aria-hidden="true">PG</span>
<div>
<p class="card__name">
Priya Ganguly
<span class="verified" title="Verified customer">✓ Verified</span>
</p>
<p class="card__project">Basement finish · 900 sq ft</p>
</div>
</div>
<span class="stars" role="img" aria-label="4 out of 5 stars" style="--fill: 80%">★★★★★</span>
</div>
<p class="card__text">
Turned an unfinished basement into a real living space with an egress window and a wet
bar. Framing and drywall are tight. Knocked a star for some paint touch-ups I had to
request after walkthrough.
</p>
<time class="card__date" datetime="2026-01-19">January 19, 2026</time>
</li>
</ul>
<p class="empty" id="empty" hidden>No reviews match this rating yet.</p>
</main>
<script src="script.js"></script>
</body>
</html>Customer Reviews
A sturdy, hi-vis reviews block for a general contractor or trades business. The header carries the aggregate score, a star row, the total review count and a set of per-star breakdown bars so visitors can scan sentiment at a glance.
Below it, a toolbar lets you filter by star rating and sort by most recent or highest rated. Each review card shows the reviewer, their project type (roofing, kitchen remodel, concrete pour and more), a verified badge, the date and the full comment. Filtering and sorting update the visible set and a live result count without any page reload.
Everything is hand-built with semantic markup, focus styles and a responsive layout that stacks the breakdown and toolbar cleanly on narrow screens. No libraries — just vanilla JS.
Illustrative UI only — not for real business use.