Building the review UI with Alpine.js, correct star semantics, and sorting
Magento's native review feature is functionally solid but visually and interactively plain. Building a review UI that actually contributes to purchase decisions means treating the review form as an Alpine component, marking up the star display with correct semantics, and offering sort and filter options without complicating the underlying data model.
Table of Contents
- 1. Native reviews versus a third party module
- 2. The review form as an Alpine component
- 3. Star display with correct semantics
- 4. Sort and filter options for reviews
- 5. Approval workflow and unpublished reviews
- 6. Loading reviews through GraphQL
- 7. Performance considerations for products with many reviews
- 8. Verified purchase badges and other trust signals
- 9. Common mistakes in the review UI
- 10. Summary
- 11. FAQ
1. Native reviews versus a third party module
Magento ships a fully featured review system with Magento_Review: a backend approval workflow, rating options per attribute, store assignment, and a solid data layer built on review, review_entity, and rating_option_vote. For many shops that is entirely enough, especially when a manual approval step is desired anyway to filter out spam and unsuitable content.
Third party modules like Trustpilot, Yotpo, or Bazaarvoice bring automated review requests by email, photo uploads, and sometimes aggregated star ratings across multiple sales channels. Switching pays off mainly when reviews are meant to be used actively as a marketing tool, for example rich snippets in Google Ads or cross platform aggregation. For a purely on-site review system that looks good and works well, the native solution with a clean Hyva integration is usually entirely sufficient.
2. The review form as an Alpine component
Magento's native review form is built on a classic server post with no JavaScript interactivity at all, which feels too blunt for a Hyva-style user experience. A better fit is an Alpine component that makes the star selection interactive, validates the form client side, and only submits the actual post once validation passes, still as a classic form without AJAX so Magento's built in form key and CSRF protection keeps working unchanged.
It is important not to build the star selection as a purely visual widget, but to bind it to a real radio input that gets submitted with the form. Plain div based stars without form binding cause the rating to get lost on submit, a mistake that shows up surprisingly often in hurried custom implementations.
<form
method="post"
action="{{ $reviewPostUrl }}"
x-data="{ rating: 0, hover: 0, comment: '' }"
>
<fieldset class="flex items-center gap-1" role="radiogroup" aria-label="Star rating">
<template x-for="star in [1, 2, 3, 4, 5]" :key="star">
<label class="cursor-pointer">
<input
type="radio"
name="ratings[1]"
class="sr-only"
:value="star"
x-model.number="rating"
>
<svg
class="w-6 h-6"
x-bind:class="(hover || rating) >= star ? 'text-amber-400' : 'text-gray-300'"
x-on:mouseenter="hover = star"
x-on:mouseleave="hover = 0"
aria-hidden="true"
><!-- Star icon --></svg>
</label>
</template>
</fieldset>
<p class="sr-only" role="status" x-text="`Rating: ${rating} out of 5 stars`"></p>
<textarea name="detail" x-model="comment" required></textarea>
<button type="submit" :disabled="rating === 0">Submit review</button>
</form>
3. Star display with correct semantics
A plain SVG star graphic without accompanying text is worthless for screen readers and cannot be evaluated for rich snippets by search engines. Every star display, whether in the form or in the output of existing reviews, needs a hidden text carrying the actual value, for example via an aria-label or a visually hidden span reading four out of five stars.
For Google's rich snippet display, structured data markup following schema.org AggregateRating is additionally required, embedded as JSON-LD in the head of the product page independently of the visual star component. Anyone who builds only the CSS stars but forgets the markup gives away search result visibility even though the actual review feature works flawlessly on a technical level.
<div class="flex items-center gap-1" role="img" aria-label="Rating: {{ $avgRating }} out of 5 stars">
<template x-for="i in 5" :key="i">
<svg
class="w-4 h-4"
x-bind:class="i <= Math.round({{ $avgRating }}) ? 'text-amber-400' : 'text-gray-300'"
aria-hidden="true"
><!-- Star icon --></svg>
</template>
<span class="text-sm text-gray-500">({{ $reviewCount }})</span>
</div>
4. Sort and filter options for reviews
With more than a handful of reviews per product, a plain chronological list gets confusing fast. Sorting by newest date, highest and lowest rating, plus a filter by star count, for example showing only five star reviews, belong to the standard expectations of a modern review UI and can be implemented entirely client side in Alpine, provided every review has already been loaded server side for the page.
For products with a very large number of reviews, say several hundred, server side pagination combined with GraphQL is the better fit instead, since purely client side sorting of every record in the browser otherwise costs unnecessary initial load time. As a rule of thumb: up to about fifty reviews, client side sorting in Alpine works perfectly well, beyond that a server side solution becomes noticeably faster.
function reviewList(reviews) {
return {
reviews,
sortBy: 'newest',
filterStars: 0,
get filtered() {
let list = this.filterStars
? this.reviews.filter((r) => r.rating === this.filterStars)
: this.reviews;
return [...list].sort((a, b) => {
if (this.sortBy === 'newest') return b.date - a.date;
if (this.sortBy === 'highest') return b.rating - a.rating;
if (this.sortBy === 'lowest') return a.rating - b.rating;
return 0;
});
},
};
}
5. Approval workflow and unpublished reviews
The native review feature defaults to manual approval in the backend, so new reviews do not appear on the product page immediately. This needs to be communicated in the frontend, otherwise it looks to the customer as if their own review got lost somewhere in the form. A short success message right after submission, pointing to the pending review, creates clarity here without needing to change the approval process itself.
Technically this message can be implemented as a plain redirect parameter after the form post, since Magento redirects back to the product page after a successful save anyway. A small Alpine fragment that reacts to a query parameter and displays a success message is enough for this purpose and needs no additional server logic.
6. Loading reviews through GraphQL
For product pages that are mostly rendered through GraphQL, the products query with its reviews field is a good fit for loading ratings consistently with the rest of the page, instead of needing an extra server side block call. That reduces the number of different data sources on the page and simplifies caching, since the product page as a whole updates through the same mechanism.
When it comes to submitting a new review, the classic form post remains the more robust choice regardless, because createProductReview as a GraphQL mutation requires a logged in customer session and guest reviews are not supported through GraphQL. For a form that needs to be open to both guests and customers, the server post therefore stays the common denominator.
7. Performance considerations for products with many reviews
If every review of a product gets fully rendered into the DOM on first page load, page size grows noticeably for popular products with hundreds of reviews, even though most customers only ever read the first few. A sensible limit is around ten to fifteen initially visible reviews with Alpine-driven loading of further entries on click, instead of transferring everything at once.
Images attached to reviews, where the review module supports them, should always ship with loading lazy, since they usually sit well below the visible viewport. Applying the same lazy loading discipline here as for the product images themselves keeps the review section from unnecessarily dragging down the product page's load time.
8. Verified purchase badges and other trust signals
A verified purchase badge next to a review noticeably raises credibility, but it is not provided for in Magento's native data model, the review table carries no link to a specific order at all. Anyone wanting to offer this signal needs to check, server side at the time a review is saved, whether the logged in customer session has a completed order containing the reviewed product, and store the result as a custom attribute on the review itself.
The same applies to a helpful vote under individual reviews, which Magento also does not ship natively. Technically this can be added with a lean custom table and a small Alpine component per review, and it matters to prevent the same visitor from voting multiple times, at least through a cookie or, for logged in customers, through the customer id, otherwise the vote count quickly loses its meaning.
9. Common mistakes in the review UI
The most common mistake is a purely visual star component with no form binding, so the actual rating gets lost on submit. Right behind it comes forgetting the aria attributes, which leaves the rating invisible to screen reader users even though it looks perfect visually. Both are easy to avoid from the start with the patterns shown above.
Just as often, the AggregateRating markup gets forgotten or left stale, for example when the average rating changes but the JSON-LD comes from a cached block fragment that is not kept in sync with the actual review data. Regularly checking that the visible star display and the structured markup match, ideally rendered from the same data source, reliably prevents this discrepancy.
| Approach | Approval Workflow | Extra Cost | Best Fit |
|---|---|---|---|
| Native Magento reviews | Manual in the backend | None | On-site reviews without a marketing focus |
| Trustpilot / Yotpo | Automated by email | Monthly license | Marketing, multi channel aggregation |
| GraphQL display, server post form | Same as native reviews | None | GraphQL-heavy product pages |
| Purely client side sorting | n/a | None | Up to roughly 50 reviews per product |
| Server side pagination | n/a | Extra endpoint | Products with very many reviews |
Mironsoft
Hyvä theme development and Luma migration
Still running Luma, or a Hyvä theme that just doesn't feel right?
We build Hyvä themes for Magento from scratch or migrate existing Luma shops cleanly, with Tailwind CSS, Alpine.js, and none of the unnecessary JavaScript baggage.
Luma-to-Hyvä Migration
Move an existing shop to Hyvä in a structured way, without losing functionality.
Custom Theme Development
Build a custom Hyvä theme from scratch based on your design.
Performance Optimization
Improve Core Web Vitals and load times in the Hyvä frontend with purpose.
10. Summary
Review UI in Hyva: Key Takeaways
Native reviews are often enough
Third party modules mainly pay off for active marketing, not for plain on-site display.
Do not forget form binding
The star selection must bind to a real radio input, otherwise the rating gets lost on submit.
Semantics before looks
aria attributes and AggregateRating markup are mandatory for accessibility and rich snippets.
Watch scale
Beyond roughly 50 reviews, server side pagination beats purely client side sorting.