Reviews Display
Star-rated reviews with breakdown bars and Schema.org markup. Replaces Loox basic.
Live preview
See it in action.
Fully interactive, drag, click, scroll inside the frame, toggle to mobile.
About this section
A polished reviews section with aggregate-rating header, 5-bar breakdown, filter-by-stars chips, sort-by-newest/highest/lowest, and Schema.org Review + AggregateRating markup so Google rich snippets pick up your stars in search results. Reviews are theme blocks, paste in your hand-picked best ones. For full review collection at scale, pair with Judge.me / Loox / Stamped exports.
Install in 90 seconds
- 01
Create /sections/modblo-reviews-display.liquid.
- 02
Paste the section code and save.
- 03
Add the section to a /products/[handle] template, /pages/reviews page, or anywhere else.
- 04
Add Review blocks for each review you want displayed. Set rating (1-5), title, body, author, date, and verified flag.
- 05
(Optional) For automatic review collection at scale, install Judge.me / Loox / Stamped and replace the inner block loop with their feed.
The Liquid
{%- comment -%}
modblo. Reviews Display
Replaces Loox basic / Judge.me free / Stamped reviews display layouts.
Reviews are theme blocks (no third-party review API integration), so
merchants paste in curated, hand-picked reviews. Includes:
- Aggregate rating header with breakdown bars
- Filter by star rating
- Sort by newest / highest / lowest
- Schema.org Review markup for SEO (rich snippets)
For full review collection at scale, pair with Judge.me / Loox / Stamped
and replace the inner block loop with their export feed.
{%- endcomment -%}
{%- assign total = 0 -%}
{%- assign sum = 0 -%}
{%- assign s5 = 0 -%}
{%- assign s4 = 0 -%}
{%- assign s3 = 0 -%}
{%- assign s2 = 0 -%}
{%- assign s1 = 0 -%}
{%- for block in section.blocks -%}
{%- if block.type == 'review' -%}
{%- assign total = total | plus: 1 -%}
{%- assign r = block.settings.rating | plus: 0 -%}
{%- assign sum = sum | plus: r -%}
{%- if r == 5 -%}{%- assign s5 = s5 | plus: 1 -%}{%- endif -%}
{%- if r == 4 -%}{%- assign s4 = s4 | plus: 1 -%}{%- endif -%}
{%- if r == 3 -%}{%- assign s3 = s3 | plus: 1 -%}{%- endif -%}
{%- if r == 2 -%}{%- assign s2 = s2 | plus: 1 -%}{%- endif -%}
{%- if r == 1 -%}{%- assign s1 = s1 | plus: 1 -%}{%- endif -%}
{%- endif -%}
{%- endfor -%}
{%- assign avg = 0 -%}
{%- if total > 0 -%}
{%- assign avg = sum | times: 10 | divided_by: total | divided_by: 10.0 -%}
{%- endif -%}
<section class="modblo-rv" data-modblo-rv
data-section-id="{{ section.id }}"
style="--modblo-rv-bg: {{ section.settings.bg }};
--modblo-rv-fg: {{ section.settings.fg }};
--modblo-rv-star: {{ section.settings.star_color }};
--modblo-rv-accent: {{ section.settings.accent }};">
<div class="modblo-rv__inner page-width" itemscope itemtype="https://schema.org/Product">
{%- if product != blank -%}
<meta itemprop="name" content="{{ product.title | escape }}">
{%- endif -%}
<header class="modblo-rv__head">
{%- if section.settings.eyebrow != blank -%}
<p class="modblo-rv__eyebrow">{{ section.settings.eyebrow }}</p>
{%- endif -%}
<h2 class="modblo-rv__h">{{ section.settings.heading | default: 'What customers say' }}</h2>
</header>
{%- if total > 0 -%}
<div class="modblo-rv__summary" itemprop="aggregateRating" itemscope itemtype="https://schema.org/AggregateRating">
<div class="modblo-rv__avg">
<p class="modblo-rv__avg-num">
<span itemprop="ratingValue">{{ avg }}</span>
<span class="modblo-rv__avg-of">/ 5</span>
</p>
<div class="modblo-rv__stars modblo-rv__stars--lg" aria-label="{{ avg }} out of 5 stars">
{%- for n in (1..5) -%}
<span class="modblo-rv__star {% if avg >= n %}is-full{% elsif avg >= n | minus: 0.5 %}is-half{% endif %}">
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 .5l2.4 4.9 5.4.8-3.9 3.8.9 5.4L8 12.9l-4.8 2.5.9-5.4L.2 6.2l5.4-.8z"/></svg>
</span>
{%- endfor -%}
</div>
<p class="modblo-rv__avg-count">
<span itemprop="reviewCount">{{ total }}</span> {% if total == 1 %}review{% else %}reviews{% endif %}
</p>
</div>
<div class="modblo-rv__breakdown">
{%- assign rows = '5|4|3|2|1' | split: '|' -%}
{%- for n in rows -%}
{%- assign nint = n | plus: 0 -%}
{%- case nint -%}
{%- when 5 -%}{%- assign cnt = s5 -%}
{%- when 4 -%}{%- assign cnt = s4 -%}
{%- when 3 -%}{%- assign cnt = s3 -%}
{%- when 2 -%}{%- assign cnt = s2 -%}
{%- when 1 -%}{%- assign cnt = s1 -%}
{%- endcase -%}
{%- assign pct = 0 -%}
{%- if total > 0 -%}{%- assign pct = cnt | times: 100 | divided_by: total -%}{%- endif -%}
<button type="button" class="modblo-rv__row" data-modblo-rv-filter="{{ n }}" aria-label="Show {{ n }}-star reviews">
<span class="modblo-rv__row-label">{{ n }}</span>
<svg class="modblo-rv__row-star" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 .5l2.4 4.9 5.4.8-3.9 3.8.9 5.4L8 12.9l-4.8 2.5.9-5.4L.2 6.2l5.4-.8z"/></svg>
<span class="modblo-rv__row-bar"><span style="width: {{ pct }}%"></span></span>
<span class="modblo-rv__row-count">{{ cnt }}</span>
</button>
{%- endfor -%}
</div>
</div>
{%- comment -%} Filter / sort controls {%- endcomment -%}
<div class="modblo-rv__controls">
<div class="modblo-rv__chips">
<button type="button" class="modblo-rv__chip is-active" data-modblo-rv-filter="all">All ({{ total }})</button>
{%- if s5 > 0 -%}<button type="button" class="modblo-rv__chip" data-modblo-rv-filter="5">5 stars</button>{%- endif -%}
{%- if s4 > 0 -%}<button type="button" class="modblo-rv__chip" data-modblo-rv-filter="4">4 stars</button>{%- endif -%}
{%- if s3 > 0 -%}<button type="button" class="modblo-rv__chip" data-modblo-rv-filter="3">3 stars</button>{%- endif -%}
</div>
<label class="modblo-rv__sort">
Sort by
<select data-modblo-rv-sort>
<option value="newest">Newest</option>
<option value="highest">Highest rated</option>
<option value="lowest">Lowest rated</option>
</select>
</label>
</div>
<div class="modblo-rv__list" data-modblo-rv-list>
{%- for block in section.blocks -%}
{%- if block.type == 'review' -%}
{%- assign r = block.settings.rating | plus: 0 -%}
<article class="modblo-rv__item"
data-modblo-rv-item
data-rating="{{ r }}"
data-date="{{ block.settings.date }}"
itemprop="review" itemscope itemtype="https://schema.org/Review"
{{ block.shopify_attributes }}>
<header class="modblo-rv__item-head">
<div class="modblo-rv__stars" itemprop="reviewRating" itemscope itemtype="https://schema.org/Rating">
<meta itemprop="ratingValue" content="{{ r }}">
<meta itemprop="bestRating" content="5">
{%- for n in (1..5) -%}
<span class="modblo-rv__star {% if r >= n %}is-full{% endif %}">
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 .5l2.4 4.9 5.4.8-3.9 3.8.9 5.4L8 12.9l-4.8 2.5.9-5.4L.2 6.2l5.4-.8z"/></svg>
</span>
{%- endfor -%}
</div>
{%- if block.settings.date != blank -%}
<time class="modblo-rv__item-date" datetime="{{ block.settings.date }}">
{{ block.settings.date | date: '%b %-d, %Y' }}
</time>
{%- endif -%}
</header>
{%- if block.settings.title != blank -%}
<h3 class="modblo-rv__item-title" itemprop="name">{{ block.settings.title }}</h3>
{%- endif -%}
<p class="modblo-rv__item-body" itemprop="reviewBody">{{ block.settings.body }}</p>
<footer class="modblo-rv__item-foot">
<span class="modblo-rv__item-author" itemprop="author" itemscope itemtype="https://schema.org/Person">
<span itemprop="name">{{ block.settings.author }}</span>
{%- if block.settings.location != blank -%}
<span class="modblo-rv__item-loc">{{ block.settings.location }}</span>
{%- endif -%}
</span>
{%- if block.settings.verified -%}
<span class="modblo-rv__verified">
<svg viewBox="0 0 16 16" width="11" height="11" fill="currentColor" aria-hidden="true"><path d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0zm3.78 6.28-4.5 4.5a.75.75 0 0 1-1.06 0L4.22 8.78a.75.75 0 1 1 1.06-1.06L7 9.44l3.97-3.97a.75.75 0 1 1 1.06 1.06z"/></svg>
Verified buyer
</span>
{%- endif -%}
</footer>
</article>
{%- endif -%}
{%- endfor -%}
</div>
<p class="modblo-rv__empty" data-modblo-rv-empty hidden>No reviews match this filter.</p>
{%- else -%}
<p class="modblo-rv__none">Add Review blocks in the theme editor to populate this section.</p>
{%- endif -%}
</div>
</section>
<style>
.modblo-rv { background: var(--modblo-rv-bg, #fff); color: var(--modblo-rv-fg, #0b0b0c); padding: clamp(48px, 6vw, 96px) 0; }
.modblo-rv__inner { max-width: 880px; margin: 0 auto; padding: 0 24px; }
.modblo-rv__head { text-align: center; margin-bottom: 36px; }
.modblo-rv__eyebrow { text-transform: uppercase; letter-spacing: .18em; font-size: 12px; font-weight: 700; color: var(--modblo-rv-accent, #6366f1); margin: 0 0 12px; }
.modblo-rv__h { font-size: clamp(28px, 4vw, 36px); letter-spacing: -.02em; margin: 0; }
.modblo-rv__summary {
display: grid; grid-template-columns: auto 1fr; gap: 40px; align-items: center;
background: color-mix(in oklab, var(--modblo-rv-fg) 3%, transparent);
border-radius: 16px; padding: 24px 28px; margin-bottom: 32px;
}
.modblo-rv__avg { text-align: center; padding-right: 24px; border-right: 1px solid color-mix(in oklab, var(--modblo-rv-fg) 8%, transparent); }
.modblo-rv__avg-num { font-size: 40px; font-weight: 700; letter-spacing: -.02em; line-height: 1; margin: 0 0 6px; font-variant-numeric: tabular-nums; }
.modblo-rv__avg-of { font-size: 16px; opacity: .4; font-weight: 500; }
.modblo-rv__avg-count { font-size: 12px; opacity: .6; margin: 6px 0 0; }
.modblo-rv__stars { display: inline-flex; gap: 2px; color: var(--modblo-rv-star, #f59e0b); }
.modblo-rv__stars--lg { gap: 3px; }
.modblo-rv__stars--lg .modblo-rv__star svg { width: 18px; height: 18px; }
.modblo-rv__star { opacity: .25; }
.modblo-rv__star.is-full { opacity: 1; }
.modblo-rv__star.is-half { opacity: .65; }
.modblo-rv__star svg { width: 14px; height: 14px; display: block; }
.modblo-rv__breakdown { display: flex; flex-direction: column; gap: 4px; }
.modblo-rv__row {
display: grid; grid-template-columns: 16px 12px 1fr 32px; gap: 8px; align-items: center;
background: transparent; border: 0; color: inherit; cursor: pointer;
padding: 4px 6px; border-radius: 6px; font-size: 12px;
transition: background .15s;
}
.modblo-rv__row:hover { background: color-mix(in oklab, var(--modblo-rv-fg) 5%, transparent); }
.modblo-rv__row-label { text-align: center; font-weight: 600; }
.modblo-rv__row-star { color: var(--modblo-rv-star, #f59e0b); width: 12px; height: 12px; }
.modblo-rv__row-bar {
display: block; height: 6px; border-radius: 999px;
background: color-mix(in oklab, var(--modblo-rv-fg) 8%, transparent); overflow: hidden;
}
.modblo-rv__row-bar > span {
display: block; height: 100%;
background: var(--modblo-rv-star, #f59e0b);
border-radius: 999px;
}
.modblo-rv__row-count { text-align: right; opacity: .65; font-variant-numeric: tabular-nums; }
.modblo-rv__controls {
display: flex; align-items: center; justify-content: space-between; gap: 16px;
margin-bottom: 24px; flex-wrap: wrap;
}
.modblo-rv__chips { display: flex; flex-wrap: wrap; gap: 6px; }
.modblo-rv__chip {
background: color-mix(in oklab, var(--modblo-rv-fg) 5%, transparent);
color: inherit; border: 0; cursor: pointer;
padding: 6px 12px; border-radius: 999px;
font-size: 12px; font-weight: 600;
transition: background .2s;
}
.modblo-rv__chip:hover { background: color-mix(in oklab, var(--modblo-rv-fg) 10%, transparent); }
.modblo-rv__chip.is-active {
background: var(--modblo-rv-fg, #0b0b0c); color: var(--modblo-rv-bg, #fff);
}
.modblo-rv__sort { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; opacity: .7; }
.modblo-rv__sort select {
background: transparent; color: inherit;
border: 1px solid color-mix(in oklab, var(--modblo-rv-fg) 15%, transparent);
border-radius: 8px; padding: 5px 8px; font-size: 12px;
}
.modblo-rv__list { display: flex; flex-direction: column; gap: 16px; }
.modblo-rv__item {
background: var(--modblo-rv-bg, #fff);
border: 1px solid color-mix(in oklab, var(--modblo-rv-fg) 8%, transparent);
border-radius: 14px; padding: 18px 20px;
}
.modblo-rv__item[hidden] { display: none; }
.modblo-rv__item-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.modblo-rv__item-date { font-size: 11px; opacity: .55; }
.modblo-rv__item-title { font-size: 15px; font-weight: 600; margin: 0 0 6px; }
.modblo-rv__item-body { font-size: 14px; line-height: 1.6; opacity: .85; margin: 0 0 12px; }
.modblo-rv__item-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.modblo-rv__item-author { font-size: 13px; }
.modblo-rv__item-loc { opacity: .5; margin-left: 6px; }
.modblo-rv__verified {
display: inline-flex; align-items: center; gap: 4px;
color: #16a34a; font-size: 11px; font-weight: 600;
}
.modblo-rv__empty, .modblo-rv__none { text-align: center; padding: 48px 0; opacity: .55; }
@media (max-width: 640px) {
.modblo-rv__summary { grid-template-columns: 1fr; gap: 20px; padding: 20px; }
.modblo-rv__avg { padding: 0 0 20px; border-right: 0; border-bottom: 1px solid color-mix(in oklab, var(--modblo-rv-fg) 8%, transparent); }
}
</style>
<script>
(function () {
var root = document.querySelector('[data-modblo-rv][data-section-id="{{ section.id }}"]');
if (!root) return;
var list = root.querySelector('[data-modblo-rv-list]');
var items = Array.from(root.querySelectorAll('[data-modblo-rv-item]'));
var empty = root.querySelector('[data-modblo-rv-empty]');
var chips = root.querySelectorAll('[data-modblo-rv-filter]');
var sort = root.querySelector('[data-modblo-rv-sort]');
var activeFilter = 'all';
var activeSort = 'newest';
function render() {
var visible = 0;
items.forEach(function (item) {
var rating = parseInt(item.dataset.rating, 10);
var match = activeFilter === 'all' || rating === parseInt(activeFilter, 10);
item.hidden = !match;
if (match) visible++;
});
empty.hidden = visible > 0;
// Sort by reordering DOM
var sorted = items.slice().sort(function (a, b) {
var ra = parseInt(a.dataset.rating, 10);
var rb = parseInt(b.dataset.rating, 10);
var da = new Date(a.dataset.date || 0).getTime();
var db = new Date(b.dataset.date || 0).getTime();
if (activeSort === 'highest') return rb - ra || db - da;
if (activeSort === 'lowest') return ra - rb || db - da;
return db - da; // newest
});
sorted.forEach(function (item) { list.appendChild(item); });
}
chips.forEach(function (chip) {
chip.addEventListener('click', function () {
// Top-row chips
if (chip.classList.contains('modblo-rv__chip')) {
activeFilter = chip.dataset.cbRvFilter;
root.querySelectorAll('.modblo-rv__chip').forEach(function (c) {
c.classList.toggle('is-active', c.dataset.cbRvFilter === activeFilter);
});
} else {
// Breakdown row click — filter by that rating
activeFilter = chip.dataset.cbRvFilter;
root.querySelectorAll('.modblo-rv__chip').forEach(function (c) {
c.classList.toggle('is-active', c.dataset.cbRvFilter === activeFilter);
});
}
render();
});
});
sort.addEventListener('change', function () {
activeSort = sort.value;
render();
});
render();
})();
</script>
{% schema %}
{
"name": "Reviews Display",
"tag": "section",
"settings": [
{ "type": "header", "content": "Heading" },
{ "type": "text", "id": "eyebrow", "label": "Eyebrow", "default": "Reviews" },
{ "type": "text", "id": "heading", "label": "Heading", "default": "What customers say" },
{ "type": "header", "content": "Colors" },
{ "type": "color", "id": "bg", "label": "Background", "default": "#ffffff" },
{ "type": "color", "id": "fg", "label": "Foreground", "default": "#0b0b0c" },
{ "type": "color", "id": "star_color", "label": "Star color", "default": "#f59e0b" },
{ "type": "color", "id": "accent", "label": "Accent", "default": "#6366f1" }
],
"blocks": [
{
"type": "review",
"name": "Review",
"settings": [
{ "type": "range", "id": "rating", "label": "Rating", "min": 1, "max": 5, "step": 1, "default": 5 },
{ "type": "text", "id": "title", "label": "Review title" },
{ "type": "textarea", "id": "body", "label": "Review body" },
{ "type": "text", "id": "author", "label": "Author name" },
{ "type": "text", "id": "location", "label": "Author location (optional)" },
{ "type": "text", "id": "date", "label": "Date (ISO)", "info": "e.g. 2026-04-12" },
{ "type": "checkbox", "id": "verified", "label": "Verified buyer badge", "default": true }
]
}
],
"presets": [{
"name": "Reviews Display",
"blocks": [
{ "type": "review", "settings": { "rating": 5, "title": "Worth every penny.", "body": "Best merino piece I own. Holds shape after a dozen washes, no pilling.", "author": "Sarah K.", "location": "Brooklyn, NY", "date": "2026-04-12" } },
{ "type": "review", "settings": { "rating": 5, "title": "Buying another in every color.", "body": "Lightweight enough for spring layers, warm enough for fall. Cut runs true.", "author": "Marcus W.", "location": "Austin, TX", "date": "2026-04-08" } },
{ "type": "review", "settings": { "rating": 4, "title": "Great fabric, runs slightly slim.", "body": "Sized up and it's perfect. Material feels premium, washes well.", "author": "Yuki T.", "location": "Portland, OR", "date": "2026-03-29" } },
{ "type": "review", "settings": { "rating": 5, "title": "Replaced three other crews.", "body": "Soft, structured, and the neck doesn't stretch out. Will reorder.", "author": "Priya R.", "location": "Toronto, ON", "date": "2026-03-21" } }
]
}]
}
{% endschema %}Unlock the section code
Reviews Display is a premium section. Get the full Liquid + scoped CSS paste-ready.
One-time purchase · Lifetime updates · You own the code
Theme editor settings
| Setting | Type | Default |
|---|---|---|
Eyebrow eyebrow | text | Reviews |
Heading heading | text | What customers say |
Background bg | color | #ffffff |
Foreground fg | color | #0b0b0c |
Star color star_color | color | #f59e0b |
Accent accent | color | #6366f1 |
Reviews (theme blocks) blocks Add unlimited Review blocks. Each carries rating, title, body, author, location, date, and verified-buyer flag. | blocks | , |
SEO & accessibility notes
- Schema.org Review and AggregateRating markup applied to every review and to the overall summary, Google picks up stars in search results.
- Real <button> for filter chips and breakdown rows, full keyboard navigation.
- Real <select> for sort, native dropdown, no custom widget.
- Stars are inline SVG (no icon font / no external request).
- All review content rendered server-side, fully crawlable.
Related
