Shop — Reviews + Rating Breakdown
A credible storefront reviews block pairing an average-rating summary with a clickable 5-star distribution breakdown, star and recency filters, four sort modes, and individual review cards carrying avatars, verified badges, dates, and helpful votes. A star-input form lets shoppers publish a new review that updates the average, the bars, and the list in real time, with load-more paging and accessible keyboard controls throughout for trustworthy social proof.
MCP
Code
:root{
--bg:#ffffff;
--ink:#16181d;
--ink-2:#2b2f3a;
--muted:#6b7280;
--brand:#3457ff;
--brand-d:#2742d6;
--sale:#e0245e;
--ok:#1f9d55;
--star:#f5a623;
--star-off:#d8dbe4;
--line:rgba(16,18,29,.1);
--line-2:rgba(16,18,29,.06);
--soft:#f6f7fb;
--soft-2:#eef1f8;
--radius:16px;
--radius-sm:11px;
--shadow:0 1px 2px rgba(16,18,29,.05), 0 10px 30px rgba(16,18,29,.07);
--maxw:920px;
}
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
margin:0;
font-family:"Inter",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
color:var(--ink);
background:
radial-gradient(1100px 540px at 90% -10%, #eef2ff 0%, transparent 60%),
radial-gradient(900px 460px at -10% 0%, #fff4f7 0%, transparent 55%),
var(--bg);
line-height:1.5;
-webkit-font-smoothing:antialiased;
-moz-osx-font-smoothing:grayscale;
min-height:100vh;
}
svg{display:block;max-width:100%}
button{font:inherit;color:inherit;cursor:pointer}
h1,h2,h3,p,ul{margin:0}
ul{list-style:none;padding:0}
.wrap{max-width:var(--maxw);margin:0 auto;padding:clamp(20px,4vw,44px) clamp(16px,4vw,32px) 64px}
/* ---------- Head ---------- */
.eyebrow{
font-size:.74rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;
color:var(--brand);margin-bottom:6px;
}
.head-top{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.head h1{font-size:clamp(1.5rem,4.4vw,2.05rem);font-weight:800;letter-spacing:-.02em}
.head-sub{color:var(--muted);margin-top:6px;font-size:.95rem}
.chip{
font-size:.72rem;font-weight:700;padding:4px 10px;border-radius:999px;
letter-spacing:.02em;white-space:nowrap;
}
.chip-ok{color:var(--ok);background:rgba(31,157,85,.12)}
.card{
background:#fff;border:1px solid var(--line);border-radius:var(--radius);
box-shadow:var(--shadow);
}
.block{margin-top:22px;display:grid;gap:18px}
/* ---------- Summary ---------- */
.summary{
display:grid;grid-template-columns:minmax(180px,230px) 1fr;gap:clamp(20px,4vw,40px);
padding:clamp(20px,3.5vw,30px);align-items:center;
}
.summary-avg{
text-align:center;padding-right:clamp(20px,4vw,40px);
border-right:1px solid var(--line-2);
}
.avg-num{font-size:3.5rem;font-weight:800;line-height:1;letter-spacing:-.03em}
.avg-stars{display:flex;justify-content:center;gap:3px;margin:8px 0 6px}
.avg-stars .s{width:22px;height:22px;color:var(--star-off)}
.avg-stars .s.on{color:var(--star)}
.avg-stars .s.half{position:relative;color:var(--star-off)}
.avg-stars .s.half::after{
content:"";position:absolute;inset:0;width:50%;overflow:hidden;
background:currentColor;-webkit-mask:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M10 1.6l2.47 5.01 5.53.8-4 3.9.94 5.5L10 14.2l-4.94 2.6.94-5.5-4-3.9 5.53-.8L10 1.6Z"/></svg>') no-repeat;
color:var(--star);
}
.avg-count{color:var(--muted);font-size:.86rem;font-weight:500}
.avg-rec{margin-top:12px;font-size:.84rem;font-weight:600;color:var(--ink-2)}
.avg-rec span{color:var(--ok);font-weight:800}
.breakdown{display:grid;gap:8px}
.bar-row{
display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:12px;
background:none;border:0;padding:5px 8px;border-radius:9px;text-align:left;
transition:background .14s ease;
}
.bar-row:hover{background:var(--soft)}
.bar-row.is-on{background:var(--soft-2)}
.bar-label{display:inline-flex;align-items:center;gap:5px;font-weight:600;font-size:.86rem;width:34px}
.mini-star{width:13px;height:13px;color:var(--star)}
.bar-track{height:9px;border-radius:999px;background:var(--soft-2);overflow:hidden}
.bar-fill{
display:block;height:100%;width:var(--w);border-radius:999px;
background:linear-gradient(90deg,#f7b733,#f5a623);
transition:width .6s cubic-bezier(.22,1,.36,1);
}
.bar-pct{font-size:.78rem;font-weight:600;color:var(--muted);width:38px;text-align:right;font-variant-numeric:tabular-nums}
/* ---------- Controls ---------- */
.controls{
display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;
}
.filters{display:flex;gap:8px;flex-wrap:wrap}
.pill{
border:1px solid var(--line);background:#fff;border-radius:999px;
padding:7px 14px;font-size:.82rem;font-weight:600;color:var(--ink-2);
transition:all .14s ease;
}
.pill:hover{border-color:var(--brand);color:var(--brand)}
.pill.is-active{background:var(--ink);color:#fff;border-color:var(--ink)}
.sort{display:inline-flex;align-items:center;gap:8px}
.sort-label{font-size:.82rem;font-weight:600;color:var(--muted)}
.sort select{
border:1px solid var(--line);background:#fff;border-radius:10px;
padding:7px 30px 7px 12px;font-size:.85rem;font-weight:600;color:var(--ink);
appearance:none;
background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 20 20" fill="%236b7280"><path d="M5 8l5 5 5-5z"/></svg>');
background-repeat:no-repeat;background-position:right 9px center;
}
.result-meta{font-size:.84rem;color:var(--muted);font-weight:500}
/* ---------- Review cards ---------- */
.reviews{display:grid;gap:14px}
.rv{
background:#fff;border:1px solid var(--line);border-radius:var(--radius-sm);
padding:18px 20px;box-shadow:0 1px 2px rgba(16,18,29,.04);
animation:rise .28s ease both;
}
@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.rv-head{display:flex;align-items:center;gap:12px}
.avatar{
width:42px;height:42px;border-radius:50%;flex:none;display:grid;place-items:center;
color:#fff;font-weight:700;font-size:.95rem;letter-spacing:.01em;
}
.rv-meta{min-width:0}
.rv-name{display:flex;align-items:center;gap:7px;font-weight:700;font-size:.95rem}
.verified{
display:inline-flex;align-items:center;gap:3px;font-size:.68rem;font-weight:700;
color:var(--ok);background:rgba(31,157,85,.1);padding:2px 7px;border-radius:999px;
}
.verified svg{width:11px;height:11px}
.rv-date{font-size:.78rem;color:var(--muted);margin-top:1px}
.rv-stars{margin-left:auto;display:flex;gap:2px;flex:none}
.rv-stars .s{width:15px;height:15px;color:var(--star-off)}
.rv-stars .s.on{color:var(--star)}
.rv-title{font-weight:700;font-size:.96rem;margin-top:12px}
.rv-body{color:var(--ink-2);font-size:.92rem;margin-top:5px}
.rv-foot{display:flex;align-items:center;gap:12px;margin-top:14px}
.helpful{
display:inline-flex;align-items:center;gap:7px;border:1px solid var(--line);
background:#fff;border-radius:999px;padding:6px 13px;font-size:.8rem;font-weight:600;
color:var(--ink-2);transition:all .14s ease;
}
.helpful svg{width:14px;height:14px}
.helpful:hover{border-color:var(--brand);color:var(--brand)}
.helpful.voted{background:var(--soft-2);border-color:var(--brand);color:var(--brand)}
.helpful.voted svg{transform:scale(1.05)}
.rv-foot-note{font-size:.78rem;color:var(--muted)}
.empty{
text-align:center;padding:40px 20px;color:var(--muted);
border:1px dashed var(--line);border-radius:var(--radius-sm);background:var(--soft);
}
.empty strong{display:block;color:var(--ink);font-size:1rem;margin-bottom:4px}
.load-wrap{text-align:center}
.btn-load{
border:1px solid var(--line);background:#fff;border-radius:999px;
padding:11px 26px;font-size:.88rem;font-weight:700;color:var(--ink);
transition:all .14s ease;
}
.btn-load:hover{border-color:var(--ink);box-shadow:var(--shadow)}
.btn-load[hidden]{display:none}
/* ---------- Write a review ---------- */
.write{padding:clamp(20px,3.5vw,28px);margin-top:6px}
.write-title{font-size:1.2rem;font-weight:800;letter-spacing:-.01em}
.write-sub{color:var(--muted);font-size:.9rem;margin-top:3px}
.field{margin-top:16px}
.field-label{display:block;font-size:.82rem;font-weight:700;color:var(--ink-2);margin-bottom:7px}
.star-input{display:flex;align-items:center;gap:4px}
.star-btn{
background:none;border:0;padding:2px;line-height:0;border-radius:6px;color:var(--star-off);
transition:transform .1s ease,color .12s ease;
}
.star-btn svg{width:30px;height:30px}
.star-btn:hover{transform:scale(1.12)}
.star-btn.on{color:var(--star)}
.star-input-hint{margin-left:10px;font-size:.82rem;font-weight:600;color:var(--muted)}
.field input,.field textarea{
width:100%;border:1px solid var(--line);border-radius:11px;
padding:11px 13px;font:inherit;color:var(--ink);background:#fff;resize:vertical;
transition:border-color .14s ease,box-shadow .14s ease;
}
.field input::placeholder,.field textarea::placeholder{color:#9aa0ad}
.field input:focus,.field textarea:focus{
outline:none;border-color:var(--brand);box-shadow:0 0 0 3px rgba(52,87,255,.16);
}
.field.err input,.field.err textarea{border-color:var(--sale);box-shadow:0 0 0 3px rgba(224,36,94,.14)}
.write-foot{display:flex;align-items:center;justify-content:space-between;gap:14px;margin-top:20px;flex-wrap:wrap}
.trust{display:inline-flex;align-items:center;gap:7px;font-size:.82rem;font-weight:600;color:var(--muted)}
.trust .lock{width:16px;height:16px;fill:var(--ok)}
.btn-submit{
background:var(--brand);color:#fff;border:0;border-radius:11px;
padding:11px 22px;font-size:.9rem;font-weight:700;
box-shadow:0 6px 18px rgba(52,87,255,.28);transition:all .14s ease;
}
.btn-submit:hover{background:var(--brand-d);transform:translateY(-1px)}
.btn-submit:active{transform:translateY(0)}
/* ---------- Focus + toast ---------- */
:focus-visible{outline:2.5px solid var(--brand);outline-offset:2px;border-radius:6px}
.star-btn:focus-visible,.bar-row:focus-visible,.pill:focus-visible,.helpful:focus-visible{outline-offset:3px}
.toast{
position:fixed;left:50%;bottom:24px;transform:translateX(-50%) translateY(16px);
background:var(--ink);color:#fff;padding:12px 20px;border-radius:999px;
font-size:.86rem;font-weight:600;box-shadow:0 14px 40px rgba(16,18,29,.3);
opacity:0;pointer-events:none;transition:opacity .22s ease,transform .22s ease;z-index:50;
display:inline-flex;align-items:center;gap:8px;max-width:90vw;
}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
.toast .dot{width:8px;height:8px;border-radius:50%;background:var(--ok);flex:none}
/* ---------- Responsive ---------- */
@media (max-width:680px){
.summary{grid-template-columns:1fr;gap:22px}
.summary-avg{border-right:0;border-bottom:1px solid var(--line-2);padding:0 0 20px}
.controls{align-items:stretch}
.sort{justify-content:space-between}
}
@media (max-width:460px){
.rv-stars{margin-left:0;width:100%;order:3;margin-top:8px}
.rv-head{flex-wrap:wrap}
.filters{width:100%}
.write-foot{flex-direction:column;align-items:stretch}
.btn-submit{width:100%}
}
@media (prefers-reduced-motion:reduce){
*{animation-duration:.001ms!important;transition-duration:.001ms!important}
}(function () {
"use strict";
/* ---------- Data (fictional) ---------- */
var AVATARS = ["#3457ff", "#e0245e", "#1f9d55", "#7c3aed", "#0ea5e9", "#f59e0b", "#ef4444", "#14b8a6"];
var reviews = [
{ id: 1, name: "Priya N.", verified: true, stars: 5, daysAgo: 2, helpful: 41,
title: "Best pair I've owned", body: "Sound is crisp and the noise cancelling actually works on my commute. Battery easily lasts the week with daily use." },
{ id: 2, name: "Marcus T.", verified: true, stars: 5, daysAgo: 4, helpful: 28,
title: "Worth every cent", body: "Super comfortable for long calls, the cushions don't get hot. Pairing to two devices at once is a game changer." },
{ id: 3, name: "Dana K.", verified: true, stars: 4, daysAgo: 6, helpful: 17,
title: "Great, with one nitpick", body: "Audio quality is excellent and they fold up neatly. Only wish the app had a proper EQ — the presets are a bit limited." },
{ id: 4, name: "Leo R.", verified: false, stars: 3, daysAgo: 9, helpful: 6,
title: "Decent but mids feel flat", body: "Comfortable and well built. Bass is strong but vocals sit a little far back for my taste. Fine for podcasts." },
{ id: 5, name: "Sofia M.", verified: true, stars: 5, daysAgo: 12, helpful: 33,
title: "Travel essential now", body: "Took them on a 9-hour flight and barely heard the engine. Quick-charge gave me 4 hours from a 10 minute top up." },
{ id: 6, name: "Owen B.", verified: true, stars: 4, daysAgo: 15, helpful: 11,
title: "Solid all-rounder", body: "Clean design, light on the head, and the controls are intuitive. Mic could be a touch clearer outdoors." },
{ id: 7, name: "Hana L.", verified: true, stars: 2, daysAgo: 20, helpful: 4,
title: "Connection drops for me", body: "Love the sound when they work, but they disconnect from my laptop every so often. Hoping a firmware update fixes it." },
{ id: 8, name: "Ben C.", verified: false, stars: 5, daysAgo: 24, helpful: 9,
title: "Exceeded expectations", body: "Was skeptical at this price but these punch way above. The case feels premium and clicks shut nicely." },
{ id: 9, name: "Yara F.", verified: true, stars: 1, daysAgo: 31, helpful: 2,
title: "Mine arrived faulty", body: "Right earcup crackled out of the box. Support sent a replacement quickly though, so adjusting my hopes." },
{ id: 10, name: "Theo W.", verified: true, stars: 4, daysAgo: 38, helpful: 14,
title: "Comfy and clean sound", body: "Wear them most of the workday without fatigue. Transparency mode is handy when someone walks up to my desk." }
];
var PAGE = 4;
var state = { filter: "all", sort: "recent", shown: PAGE };
/* ---------- Helpers ---------- */
var $ = function (s, r) { return (r || document).querySelector(s); };
var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); };
function esc(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
});
}
function initials(name) {
var p = name.trim().split(/\s+/);
return (p[0][0] + (p[1] ? p[1][0] : "")).toUpperCase();
}
function avatarColor(id) { return AVATARS[id % AVATARS.length]; }
function plural(n, w) { return n + " " + w + (n === 1 ? "" : "s"); }
function relDate(days) {
if (days === 0) return "Today";
if (days === 1) return "Yesterday";
if (days < 7) return days + " days ago";
if (days < 30) { var w = Math.round(days / 7); return plural(w, "week") + " ago"; }
var m = Math.round(days / 30); return plural(m, "month") + " ago";
}
var STAR = '<svg viewBox="0 0 20 20" aria-hidden="true"><use href="#star"/></svg>';
function starsRow(n, cls) {
var out = "";
for (var i = 1; i <= 5; i++) out += '<span class="s ' + (i <= n ? "on" : "") + ' ' + (cls || "") + '">' + STAR + "</span>";
return out;
}
/* ---------- Toast ---------- */
var toastEl, toastTimer;
function toast(msg) {
if (!toastEl) {
toastEl = document.createElement("div");
toastEl.className = "toast";
toastEl.setAttribute("role", "status");
document.body.appendChild(toastEl);
}
toastEl.innerHTML = '<span class="dot"></span>' + esc(msg);
requestAnimationFrame(function () { toastEl.classList.add("show"); });
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2600);
}
/* ---------- Summary + breakdown ---------- */
function computeSummary() {
var counts = [0, 0, 0, 0, 0, 0];
var total = 0, sum = 0, recommend = 0;
reviews.forEach(function (r) {
counts[r.stars]++; total++; sum += r.stars;
if (r.stars >= 4) recommend++;
});
return { counts: counts, total: total, avg: total ? sum / total : 0, recPct: total ? Math.round((recommend / total) * 100) : 0 };
}
function renderSummary() {
var s = computeSummary();
$("#avgNum").textContent = s.avg.toFixed(1);
$("#avgCount").textContent = "Based on " + plural(s.total, "review");
$("#recPct").textContent = s.recPct + "%";
// average stars with half-star support
var avgEl = $("#avgStars");
avgEl.setAttribute("aria-label", s.avg.toFixed(1) + " out of 5 stars");
var html = "";
for (var i = 1; i <= 5; i++) {
var cls = "s";
if (s.avg >= i) cls += " on";
else if (s.avg >= i - 0.5) cls += " half on";
html += '<span class="' + cls + '">' + STAR + "</span>";
}
avgEl.innerHTML = html;
// distribution bars
$$(".bar-row").forEach(function (row) {
var star = +row.dataset.stars;
var c = s.counts[star];
var pct = s.total ? Math.round((c / s.total) * 100) : 0;
$(".bar-fill", row).style.setProperty("--w", pct + "%");
$(".bar-pct", row).textContent = pct + "%";
row.setAttribute("aria-label", plural(c, "review") + " at " + star + " stars, " + pct + " percent");
});
}
/* ---------- Filter + sort + render list ---------- */
function filteredSorted() {
var list = reviews.filter(function (r) {
return state.filter === "all" || r.stars === +state.filter;
});
var by = {
recent: function (a, b) { return a.daysAgo - b.daysAgo; },
helpful: function (a, b) { return b.helpful - a.helpful; },
high: function (a, b) { return b.stars - a.stars || a.daysAgo - b.daysAgo; },
low: function (a, b) { return a.stars - b.stars || a.daysAgo - b.daysAgo; }
};
return list.slice().sort(by[state.sort]);
}
function reviewCard(r) {
var li = document.createElement("li");
li.className = "rv";
li.innerHTML =
'<div class="rv-head">' +
'<span class="avatar" style="background:' + avatarColor(r.id) + '" aria-hidden="true">' + esc(initials(r.name)) + "</span>" +
'<div class="rv-meta">' +
'<div class="rv-name">' + esc(r.name) +
(r.verified ? '<span class="verified"><svg viewBox="0 0 20 20" aria-hidden="true"><path fill="currentColor" d="M8.2 13.3 5 10.1l1.4-1.4 1.8 1.8 5-5L14.6 7l-6.4 6.3Z"/></svg>Verified</span>' : "") +
"</div>" +
'<div class="rv-date">' + relDate(r.daysAgo) + "</div>" +
"</div>" +
'<div class="rv-stars" role="img" aria-label="' + r.stars + ' out of 5 stars">' + starsRow(r.stars) + "</div>" +
"</div>" +
'<h3 class="rv-title">' + esc(r.title) + "</h3>" +
'<p class="rv-body">' + esc(r.body) + "</p>" +
'<div class="rv-foot">' +
'<button class="helpful" type="button" data-id="' + r.id + '" aria-pressed="false">' +
'<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M2 10h3v11H2V10Zm6.5 11c-.7 0-1.3-.4-1.6-1H7V10c0-.3.1-.5.3-.7l6-6 .9.9c.2.2.3.5.3.8v.2L13.6 9H21c.8 0 1.5.7 1.5 1.5v2c0 .2 0 .4-.1.6l-2.3 5.5c-.3.7-.9 1.1-1.6 1.1H8.5Z"/></svg>' +
'Helpful <span class="hc">(' + r.helpful + ")</span>" +
"</button>" +
'<span class="rv-foot-note">' + plural(r.helpful, "person") + " found this helpful</span>" +
"</div>";
return li;
}
function render() {
var list = filteredSorted();
var ul = $("#reviewList");
ul.innerHTML = "";
if (!list.length) {
var empty = document.createElement("li");
empty.className = "empty";
empty.innerHTML = "<strong>No reviews match this filter</strong>Try selecting a different star rating.";
ul.appendChild(empty);
$("#loadMore").hidden = true;
} else {
list.slice(0, state.shown).forEach(function (r) { ul.appendChild(reviewCard(r)); });
$("#loadMore").hidden = state.shown >= list.length;
if (!$("#loadMore").hidden) {
$("#loadMore").textContent = "Load more reviews (" + (list.length - state.shown) + " more)";
}
}
// result meta
var shown = Math.min(state.shown, list.length);
var noun = state.filter === "all" ? "review" : state.filter + "-star review";
$("#resultMeta").textContent = "Showing " + shown + " of " + plural(list.length, noun);
}
/* ---------- Events: filter pills + breakdown rows ---------- */
function setFilter(val) {
state.filter = val;
state.shown = PAGE;
$$(".pill").forEach(function (p) {
var on = p.dataset.filter === val;
p.classList.toggle("is-active", on);
p.setAttribute("aria-pressed", on ? "true" : "false");
});
$$(".bar-row").forEach(function (row) {
row.classList.toggle("is-on", val !== "all" && row.dataset.stars === val);
});
render();
}
$$(".pill").forEach(function (p) {
p.addEventListener("click", function () { setFilter(p.dataset.filter); });
});
$$(".bar-row").forEach(function (row) {
row.addEventListener("click", function () {
var val = state.filter === row.dataset.stars ? "all" : row.dataset.stars;
setFilter(val);
});
});
$("#sort").addEventListener("change", function () {
state.sort = this.value;
state.shown = PAGE;
render();
});
$("#loadMore").addEventListener("click", function () {
state.shown += PAGE;
render();
});
/* ---------- Helpful votes (event delegation) ---------- */
var voted = {};
$("#reviewList").addEventListener("click", function (e) {
var btn = e.target.closest(".helpful");
if (!btn) return;
var id = +btn.dataset.id;
var rev = reviews.filter(function (r) { return r.id === id; })[0];
if (!rev) return;
if (voted[id]) {
rev.helpful--; voted[id] = false;
btn.classList.remove("voted");
btn.setAttribute("aria-pressed", "false");
} else {
rev.helpful++; voted[id] = true;
btn.classList.add("voted");
btn.setAttribute("aria-pressed", "true");
toast("Thanks — marked as helpful");
}
$(".hc", btn).textContent = "(" + rev.helpful + ")";
var note = btn.parentNode.querySelector(".rv-foot-note");
if (note) note.textContent = plural(rev.helpful, "person") + " found this helpful";
});
/* ---------- Star rating input ---------- */
var starBtns = $$(".star-btn");
var chosen = 0, hint = $("#starHint");
var WORDS = ["", "Poor", "Fair", "Good", "Very good", "Excellent"];
function paint(n) {
starBtns.forEach(function (b) { b.classList.toggle("on", +b.dataset.val <= n); });
}
function commit(n) {
chosen = n;
starBtns.forEach(function (b) {
b.setAttribute("aria-checked", +b.dataset.val === n ? "true" : "false");
b.tabIndex = +b.dataset.val === n ? 0 : -1;
});
if (n === 0) { starBtns[0].tabIndex = 0; }
paint(n);
hint.textContent = n ? WORDS[n] + " · " + n + "/5" : "Tap to rate";
}
commit(0);
starBtns.forEach(function (b) {
b.addEventListener("mouseenter", function () { paint(+b.dataset.val); });
b.addEventListener("click", function () { commit(+b.dataset.val); });
b.addEventListener("keydown", function (e) {
if (e.key === "ArrowRight" || e.key === "ArrowUp") {
e.preventDefault(); var n = Math.min(5, (chosen || 0) + 1); commit(n); starBtns[n - 1].focus();
} else if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
e.preventDefault(); var m = Math.max(1, (chosen || 1) - 1); commit(m); starBtns[m - 1].focus();
} else if (e.key === " " || e.key === "Enter") {
e.preventDefault(); commit(+b.dataset.val);
}
});
});
$("#starInput").addEventListener("mouseleave", function () { paint(chosen); });
/* ---------- Submit form ---------- */
var form = $("#reviewForm");
form.addEventListener("submit", function (e) {
e.preventDefault();
var nameEl = $("#rvName"), bodyEl = $("#rvBody");
var name = nameEl.value.trim(), body = bodyEl.value.trim();
nameEl.parentNode.classList.remove("err");
bodyEl.parentNode.classList.remove("err");
if (!chosen) { $("#starInput").classList.add("err"); toast("Please pick a star rating"); starBtns[0].focus(); return; }
if (!name) { nameEl.parentNode.classList.add("err"); toast("Please add your name"); nameEl.focus(); return; }
if (!body) { bodyEl.parentNode.classList.add("err"); toast("Please write your review"); bodyEl.focus(); return; }
$("#starInput").classList.remove("err");
var newId = Math.max.apply(null, reviews.map(function (r) { return r.id; })) + 1;
reviews.push({
id: newId, name: name, verified: true, stars: chosen, daysAgo: 0,
helpful: 0, title: chosen >= 4 ? "Great experience" : "My honest take",
body: body
});
// reset form, refresh UI, surface the new review
form.reset();
commit(0);
state.filter = "all";
state.sort = "recent";
$("#sort").value = "recent";
state.shown = PAGE;
setFilter("all");
renderSummary();
toast("Review published — thank you!");
$("#reviewList").scrollIntoView({ behavior: "smooth", block: "start" });
});
/* ---------- Init ---------- */
renderSummary();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Reviews + Rating Breakdown</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<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="wrap" role="main">
<header class="head">
<p class="eyebrow">Customer reviews</p>
<div class="head-top">
<h1 id="reviews-title">Aurora Wireless Headphones</h1>
<span class="chip chip-ok" aria-hidden="true">In stock</span>
</div>
<p class="head-sub">Real feedback from verified buyers — fictional storefront demo.</p>
</header>
<section class="block" aria-labelledby="reviews-title">
<!-- Summary + breakdown -->
<div class="summary card">
<div class="summary-avg">
<div class="avg-num" id="avgNum">4.6</div>
<div class="avg-stars" id="avgStars" role="img" aria-label="4.6 out of 5 stars"></div>
<div class="avg-count" id="avgCount">Based on 0 reviews</div>
<div class="avg-rec"><span id="recPct">94%</span> would recommend</div>
</div>
<div class="breakdown" role="group" aria-label="Rating distribution. Select a row to filter reviews.">
<button class="bar-row" data-stars="5" type="button">
<span class="bar-label">5 <svg class="mini-star" viewBox="0 0 20 20" aria-hidden="true"><use href="#star"/></svg></span>
<span class="bar-track"><span class="bar-fill" style="--w:0%"></span></span>
<span class="bar-pct">0%</span>
</button>
<button class="bar-row" data-stars="4" type="button">
<span class="bar-label">4 <svg class="mini-star" viewBox="0 0 20 20" aria-hidden="true"><use href="#star"/></svg></span>
<span class="bar-track"><span class="bar-fill" style="--w:0%"></span></span>
<span class="bar-pct">0%</span>
</button>
<button class="bar-row" data-stars="3" type="button">
<span class="bar-label">3 <svg class="mini-star" viewBox="0 0 20 20" aria-hidden="true"><use href="#star"/></svg></span>
<span class="bar-track"><span class="bar-fill" style="--w:0%"></span></span>
<span class="bar-pct">0%</span>
</button>
<button class="bar-row" data-stars="2" type="button">
<span class="bar-label">2 <svg class="mini-star" viewBox="0 0 20 20" aria-hidden="true"><use href="#star"/></svg></span>
<span class="bar-track"><span class="bar-fill" style="--w:0%"></span></span>
<span class="bar-pct">0%</span>
</button>
<button class="bar-row" data-stars="1" type="button">
<span class="bar-label">1 <svg class="mini-star" viewBox="0 0 20 20" aria-hidden="true"><use href="#star"/></svg></span>
<span class="bar-track"><span class="bar-fill" style="--w:0%"></span></span>
<span class="bar-pct">0%</span>
</button>
</div>
</div>
<!-- Controls -->
<div class="controls">
<div class="filters" role="group" aria-label="Filter reviews by star rating">
<button class="pill is-active" data-filter="all" type="button" aria-pressed="true">All</button>
<button class="pill" data-filter="5" type="button" aria-pressed="false">5 star</button>
<button class="pill" data-filter="4" type="button" aria-pressed="false">4 star</button>
<button class="pill" data-filter="3" type="button" aria-pressed="false">3 star</button>
<button class="pill" data-filter="2" type="button" aria-pressed="false">2 star</button>
<button class="pill" data-filter="1" type="button" aria-pressed="false">1 star</button>
</div>
<label class="sort">
<span class="sort-label">Sort</span>
<select id="sort" aria-label="Sort reviews">
<option value="recent">Most recent</option>
<option value="helpful">Most helpful</option>
<option value="high">Highest rated</option>
<option value="low">Lowest rated</option>
</select>
</label>
</div>
<p class="result-meta" id="resultMeta" aria-live="polite">Showing all reviews</p>
<!-- Review list -->
<ul class="reviews" id="reviewList" aria-live="polite"></ul>
<div class="load-wrap">
<button class="btn-load" id="loadMore" type="button">Load more reviews</button>
</div>
<!-- Write a review -->
<form class="write card" id="reviewForm" novalidate>
<h2 class="write-title">Write a review</h2>
<p class="write-sub">Share your experience with this product.</p>
<div class="field">
<span class="field-label">Your rating</span>
<div class="star-input" id="starInput" role="radiogroup" aria-label="Your rating, 1 to 5 stars">
<button type="button" class="star-btn" role="radio" data-val="1" aria-checked="false" aria-label="1 star"><svg viewBox="0 0 20 20" aria-hidden="true"><use href="#star"/></svg></button>
<button type="button" class="star-btn" role="radio" data-val="2" aria-checked="false" aria-label="2 stars"><svg viewBox="0 0 20 20" aria-hidden="true"><use href="#star"/></svg></button>
<button type="button" class="star-btn" role="radio" data-val="3" aria-checked="false" aria-label="3 stars"><svg viewBox="0 0 20 20" aria-hidden="true"><use href="#star"/></svg></button>
<button type="button" class="star-btn" role="radio" data-val="4" aria-checked="false" aria-label="4 stars"><svg viewBox="0 0 20 20" aria-hidden="true"><use href="#star"/></svg></button>
<button type="button" class="star-btn" role="radio" data-val="5" aria-checked="false" aria-label="5 stars"><svg viewBox="0 0 20 20" aria-hidden="true"><use href="#star"/></svg></button>
<span class="star-input-hint" id="starHint">Tap to rate</span>
</div>
</div>
<div class="field">
<label class="field-label" for="rvName">Name</label>
<input id="rvName" name="name" type="text" autocomplete="name" placeholder="Alex Morgan" required />
</div>
<div class="field">
<label class="field-label" for="rvBody">Review</label>
<textarea id="rvBody" name="body" rows="3" placeholder="What did you like or dislike?" required></textarea>
</div>
<div class="write-foot">
<span class="trust"><svg class="lock" viewBox="0 0 20 20" aria-hidden="true"><path d="M5 9V7a5 5 0 0 1 10 0v2h1a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1h1Zm2 0h6V7a3 3 0 0 0-6 0v2Z"/></svg>Verified purchase</span>
<button class="btn-submit" type="submit">Submit review</button>
</div>
</form>
</section>
</main>
<!-- Shared star glyph -->
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<symbol id="star" viewBox="0 0 20 20"><path d="M10 1.6l2.47 5.01 5.53.8-4 3.9.94 5.5L10 14.2l-4.94 2.6.94-5.5-4-3.9 5.53-.8L10 1.6Z"/></symbol>
</svg>
<script src="script.js"></script>
</body>
</html>Reviews + Rating Breakdown
A self-contained product-reviews module built for social proof. The summary panel shows the average score with half-star rendering, a recommend rate, and a five-row distribution where each bar is a button — click a row to filter the list to that rating, click again to clear. Filter pills and a sort menu (most recent, most helpful, highest, lowest) keep the result set in sync, with a live count and load-more paging underneath.
Each review card carries a colored avatar with initials, a verified-purchase badge, a relative date, a star row, and a helpful-vote button that toggles its count and updates the “found this helpful” note. The write-a-review form uses an accessible star-rating input (hover preview, arrow-key navigation, ARIA radiogroup) and validates name, body, and rating before publishing. Submitting prepends the new review and recomputes the average, breakdown bars, and recommend rate instantly.
Everything is vanilla JS with no dependencies: state lives in a small object, rendering is data-driven, and interactions are wired through event delegation. The layout collapses from a two-column summary to a single stacked column under 680px and stays usable down to 360px, with focus-visible rings and reduced-motion support.
Illustrative storefront UI only — fictional products, prices, and reviews. No real checkout.