Product Reviews in Hyva: Building the Review UI with Alpine.js
AI generated
Hyvä
phtml
Hyva Theme · Product Reviews
Product Reviews in Hyva Theme
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.

13 min read Review Form Star Semantics aria attributes Sorting Rating Snippets

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.

11. FAQ: Review UI in Hyva: Key Takeaways

1Is the native Magento review feature enough for most shops?
Yes, for a solid on-site review system with an approval workflow, the native feature is technically sufficient. Third party modules pay off mainly when reviews are meant to be used actively as a marketing tool across multiple channels.
2Why should the star selection in the form bind to a radio input?
Because otherwise no value gets submitted with the form and the rating is lost on submit. Plain div or svg stars without form binding are a common, easily avoidable mistake in custom implementations.
3What is AggregateRating and why is it needed?
AggregateRating is a schema.org vocabulary for structured rating data that Google evaluates for star rich snippets in search results. Without this JSON-LD markup, no stars appear in the search result, even if the reviews display flawlessly on the page itself.
4At what point does server side pagination pay off?
As a rule of thumb, up to about fifty reviews, client side sorting and filtering in Alpine performs well enough. Beyond that, a server side solution with pagination becomes noticeably faster because not every record needs to be loaded initially.
5Can I submit new reviews through GraphQL instead of a form post?
Technically yes, via the createProductReview mutation, but it requires a logged in customer session. For a form that also needs to stay open to guests, the classic server post remains the more robust and simpler solution.
6How do I communicate the approval workflow in the frontend?
With a short success message right after submission that points to the pending review. Without that hint it looks to customers as if their own review simply vanished, when it has really just not been approved yet.
7How do I make the star display accessible to screen readers?
Through a hidden text carrying the actual value, for example via an aria-label or a visually hidden span reading four out of five stars. A plain graphic without accompanying text stays meaningless for screen reader users.
8Should I load every review of a product into the DOM right away?
With few reviews, yes, with popular products carrying hundreds of reviews, better not. Ten to fifteen initially visible reviews with click-to-load-more keep page size and load time in check.
9What is the biggest difference between native reviews and Trustpilot?
Native reviews require manual approval in the backend and stay purely on-site, while Trustpilot and comparable services send automated email requests after purchase and often aggregate ratings across multiple sales channels, which comes with extra license costs.
10How do I keep the star display and structured markup from drifting apart?
By rendering both from the same data source instead of pulling the JSON-LD from a separately cached block fragment. Regularly checking both prevents a changed average rating from updating only visually but not in the markup.