Skip to content
Commerce block

Customer Reviews

Sortable reviews with verified-purchase badges, optimistic helpful votes and a real expand control instead of a fade that hides text.

274 lineslucide-reactAdded 8 Aug 2026Updated 13 Sept 2026
  • reviews
  • ratings
  • verified
  • social proof
  • ugc

What's included

  • components/review-list.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add review-list

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

4 reviews

  • Hannah T.Verified purchase

    Better than the price suggests

    I bought this expecting to return it and it has become the thing I reach for three days a week. The knit is dense without being heavy, and after four washes there is no pilling at the cuffs where I would expect it first. Sizing is honest — I am usually a medium and the medium fits with room for a shirt underneath.

    Bought M · usually M
  • Sam K.

    Nice but the colour is off

    The stone reads much warmer in person than on screen — closer to a light camel. Lovely sweater, just not the colour I was picturing.

  • Devin R.Verified purchase

    Great, but runs slightly long

    No complaints about the material at all. The body is about an inch longer than I expected, which is fine untucked but worth knowing if you are between sizes.

    Bought L · usually L
  • Priya M.Verified purchase

    Third one I have bought

    Two in stone, one in navy. They hold their shape better than anything else I own at twice the price.

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/review-list.tsx
'use client'

/**
 * <ReviewList> — customer reviews with sorting and helpful votes.
 *
 * The "verified purchase" badge is the most valuable element here and the
 * one most often left out. A review page where nothing distinguishes a
 * buyer from a passer-by is worth less than no reviews at all, because
 * readers discount all of it.
 *
 * The helpful vote is optimistic — the count moves on click and the request
 * catches up. For an action this small, waiting for a round trip before the
 * number changes reads as a broken button, and the cost of being wrong is
 * one wrong count until the next load.
 *
 * Long reviews are clamped with a real expand control rather than being cut
 * off with a fade. A gradient that hides text with no way to reveal it is a
 * dark pattern by accident.
 */

import * as React from 'react'
import { Star, BadgeCheck, ThumbsUp } from 'lucide-react'

export interface Review {
  id: string
  author: string
  rating: number
  title: string
  body: string
  /** ISO date. */
  date: string
  verified?: boolean
  helpfulCount: number
  /** Size/fit note, e.g. "Bought M · usually M". */
  context?: string
}

export interface ReviewListProps {
  reviews?: Review[]
  locale?: string
  className?: string
}

const DEFAULT_REVIEWS: Review[] = [
  {
    id: '1',
    author: 'Hannah T.',
    rating: 5,
    title: 'Better than the price suggests',
    body: 'I bought this expecting to return it and it has become the thing I reach for three days a week. The knit is dense without being heavy, and after four washes there is no pilling at the cuffs where I would expect it first. Sizing is honest — I am usually a medium and the medium fits with room for a shirt underneath.',
    date: '2026-07-28',
    verified: true,
    helpfulCount: 34,
    context: 'Bought M · usually M',
  },
  {
    id: '2',
    author: 'Devin R.',
    rating: 4,
    title: 'Great, but runs slightly long',
    body: 'No complaints about the material at all. The body is about an inch longer than I expected, which is fine untucked but worth knowing if you are between sizes.',
    date: '2026-07-14',
    verified: true,
    helpfulCount: 12,
    context: 'Bought L · usually L',
  },
  {
    id: '3',
    author: 'Priya M.',
    rating: 5,
    title: 'Third one I have bought',
    body: 'Two in stone, one in navy. They hold their shape better than anything else I own at twice the price.',
    date: '2026-06-30',
    verified: true,
    helpfulCount: 8,
  },
  {
    id: '4',
    author: 'Sam K.',
    rating: 3,
    title: 'Nice but the colour is off',
    body: 'The stone reads much warmer in person than on screen — closer to a light camel. Lovely sweater, just not the colour I was picturing.',
    date: '2026-06-11',
    helpfulCount: 21,
  },
]

type SortKey = 'recent' | 'helpful' | 'highest' | 'lowest'

function formatDate(iso: string, locale = 'en-GB'): string {
  return new Date(`${iso}T00:00:00Z`).toLocaleDateString(locale, {
    day: 'numeric',
    month: 'long',
    year: 'numeric',
    timeZone: 'UTC',
  })
}

export function ReviewList({
  reviews = DEFAULT_REVIEWS,
  locale = 'en-GB',
  className = '',
}: ReviewListProps) {
  // Per-instance ids. A literal id in a reusable component is a
  // collision waiting for the second copy on the page — and a <label>
  // then resolves to whichever input rendered first.
  const uid = React.useId()
  const [sort, setSort] = React.useState<SortKey>('helpful')
  const [voted, setVoted] = React.useState<Set<string>>(() => new Set())
  const [expanded, setExpanded] = React.useState<Set<string>>(() => new Set())

  const sorted = React.useMemo(() => {
    const copy = [...reviews]
    copy.sort((a, b) => {
      if (sort === 'recent') return b.date.localeCompare(a.date)
      if (sort === 'helpful') return b.helpfulCount - a.helpfulCount
      if (sort === 'highest') return b.rating - a.rating
      return a.rating - b.rating
    })
    return copy
  }, [reviews, sort])

  function toggleVote(id: string) {
    setVoted((prev) => {
      const next = new Set(prev)
      if (next.has(id)) next.delete(id)
      else next.add(id)
      return next
    })
  }

  function toggleExpanded(id: string) {
    setExpanded((prev) => {
      const next = new Set(prev)
      if (next.has(id)) next.delete(id)
      else next.add(id)
      return next
    })
  }

  return (
    <section id={`${uid}-reviews`} className={`w-full ${className}`}>
      <div className="flex flex-wrap items-center justify-between gap-3 border-b border-border/60 pb-4">
        <h2 className="text-lg font-bold tracking-tight">
          {reviews.length} {reviews.length === 1 ? 'review' : 'reviews'}
        </h2>

        <div className="flex items-center gap-2">
          <label htmlFor={`${uid}-review-sort`} className="text-sm text-muted-foreground">
            Sort
          </label>
          <select
            id={`${uid}-review-sort`}
            value={sort}
            onChange={(e) => setSort(e.target.value as SortKey)}
            className="rounded-xl border border-border/60 bg-background px-2.5 py-1.5 text-sm outline-none focus-visible:ring-2 focus-visible:ring-primary"
          >
            <option value="helpful">Most helpful</option>
            <option value="recent">Most recent</option>
            <option value="highest">Highest rated</option>
            <option value="lowest">Lowest rated</option>
          </select>
        </div>
      </div>

      <ul className="divide-y divide-border/40">
        {sorted.map((review) => {
          const isVoted = voted.has(review.id)
          const isExpanded = expanded.has(review.id)
          const isLong = review.body.length > 220

          return (
            <li key={review.id} className="py-6">
              <div className="flex flex-wrap items-center gap-x-3 gap-y-1.5">
                {/* One image, not five icons — see the note in
                    testimonial-grid. Without the role the label is dropped. */}
                <div
                  role="img"
                  aria-label={`${review.rating} out of 5`}
                  className="flex gap-0.5"
                >
                  {Array.from({ length: 5 }, (_, i) => (
                    <Star
                      key={i}
                      aria-hidden
                      className={
                        i < review.rating
                          ? 'h-3.5 w-3.5 fill-amber-400 text-amber-400'
                          : 'h-3.5 w-3.5 text-muted-foreground/30'
                      }
                    />
                  ))}
                </div>

                <span className="text-sm font-medium">{review.author}</span>

                {/* The element that makes the rest of the page credible. */}
                {review.verified ? (
                  <span className="inline-flex items-center gap-1 text-xs font-medium text-emerald-600 dark:text-emerald-400">
                    <BadgeCheck aria-hidden className="h-3.5 w-3.5" />
                    Verified purchase
                  </span>
                ) : null}

                <time dateTime={review.date} className="ms-auto text-xs text-muted-foreground">
                  {formatDate(review.date, locale)}
                </time>
              </div>

              {review.title ? (
                <h3 data-stress-ignore className="mt-2.5 font-semibold">{review.title}</h3>
              ) : null}

              <p
                id={`${uid}-${review.id}-body`}
                className={`mt-1.5 text-sm leading-relaxed text-muted-foreground ${
                  isLong && !isExpanded ? 'line-clamp-3' : ''
                }`}
              >
                {review.body}
              </p>

              {/* A control, not a fade that hides text with no way back. */}
              {isLong ? (
                <button
                  type="button"
                  onClick={() => toggleExpanded(review.id)}
                  aria-expanded={isExpanded}
                  /*
                    The paragraph, which is always in the document — this
                    control clamps text rather than unmounting it, so the
                    IDREF is unconditional here where the drawers and
                    listboxes elsewhere in the catalog have to guard it.
                  */
                  aria-controls={`${uid}-${review.id}-body`}
                  className="mt-1.5 text-xs font-semibold hover:underline"
                >
                  {isExpanded ? 'Show less' : 'Read more'}
                </button>
              ) : null}

              <div className="mt-3 flex flex-wrap items-center gap-4">
                {review.context ? (
                  <span className="rounded-full bg-muted px-2.5 py-1 text-xs text-muted-foreground">
                    {review.context}
                  </span>
                ) : null}

                <button
                  type="button"
                  onClick={() => toggleVote(review.id)}
                  aria-pressed={isVoted}
                  className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs transition-colors ${
                    isVoted
                      ? 'border-primary/40 bg-primary/10 text-primary'
                      : 'border-border/60 text-muted-foreground hover:text-foreground'
                  }`}
                >
                  <ThumbsUp aria-hidden className="h-3.5 w-3.5" />
                  Helpful
                  {/* Optimistic — the count moves before the request lands. */}
                  <span className="tabular-nums">
                    {review.helpfulCount + (isVoted ? 1 : 0)}
                  </span>
                </button>
              </div>
            </li>
          )
        })}
      </ul>
    </section>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Install: npm i lucide-react
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/review-list.tsx and import it where you need the section:

import { ReviewList } from '@/components/review-list'

Customize

1 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
reviewsReview[]DEFAULT_REVIEWS
localestring'en-GB'
classNamestring''

Not using React?

The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.

This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.

review-list.html
<!--
  Customer Reviews — markup from the Hoverlab catalog.

  This is the block rendered once to HTML and wrapped as a component
  file. It is not a port of the React source: the Tailwind classes carry
  the design, which is the part that took the work, and they are the same
  in every framework.

  This block is interactive in React and the handlers are NOT here.
  Buttons, toggles and menus render in their initial state and do
  nothing until you wire them up.
-->
<section id="_R_0_-reviews" class="w-full ">
  <div class="flex flex-wrap items-center justify-between gap-3 border-b border-border/60 pb-4">
    <h2 class="text-lg font-bold tracking-tight">4 reviews</h2>
    <div class="flex items-center gap-2">
      <label for="_R_0_-review-sort" class="text-sm text-muted-foreground">Sort</label>
      <select id="_R_0_-review-sort" class="rounded-xl border border-border/60 bg-background px-2.5 py-1.5 text-sm outline-none focus-visible:ring-2 focus-visible:ring-primary">
        <option value="helpful" selected="">Most helpful</option>
        <option value="recent">Most recent</option>
        <option value="highest">Highest rated</option>
        <option value="lowest">Lowest rated</option>
      </select>
    </div>
  </div>
  <ul class="divide-y divide-border/40">
    <li class="py-6">
      <div class="flex flex-wrap items-center gap-x-3 gap-y-1.5">
        <div role="img" aria-label="5 out of 5" class="flex gap-0.5">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 fill-amber-400 text-amber-400" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 fill-amber-400 text-amber-400" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 fill-amber-400 text-amber-400" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 fill-amber-400 text-amber-400" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 fill-amber-400 text-amber-400" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
        </div>
        <span class="text-sm font-medium">Hannah T.</span>
        <span class="inline-flex items-center gap-1 text-xs font-medium text-emerald-600 dark:text-emerald-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check h-3.5 w-3.5" aria-hidden="true"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"></path><path d="m9 12 2 2 4-4"></path></svg>Verified purchase</span>
        <time dateTime="2026-07-28" class="ms-auto text-xs text-muted-foreground">28 July 2026</time>
      </div>
      <h3 data-stress-ignore="true" class="mt-2.5 font-semibold">Better than the price suggests</h3>
      <p id="_R_0_-1-body" class="mt-1.5 text-sm leading-relaxed text-muted-foreground line-clamp-3">I bought this expecting to return it and it has become the thing I reach for three days a week. The knit is dense without being heavy, and after four washes there is no pilling at the cuffs where I would expect it first. Sizing is honest — I am usually a medium and the medium fits with room for a shirt underneath.</p>
      <button type="button" aria-expanded="false" aria-controls="_R_0_-1-body" class="mt-1.5 text-xs font-semibold hover:underline">Read more</button>
      <div class="mt-3 flex flex-wrap items-center gap-4">
        <span class="rounded-full bg-muted px-2.5 py-1 text-xs text-muted-foreground">Bought M · usually M</span>
        <button type="button" aria-pressed="false" class="inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs transition-colors border-border/60 text-muted-foreground hover:text-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-up h-3.5 w-3.5" aria-hidden="true"><path d="M7 10v12"></path><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"></path></svg>Helpful<span class="tabular-nums">34</span></button>
      </div>
    </li>
    <li class="py-6">
      <div class="flex flex-wrap items-center gap-x-3 gap-y-1.5">
        <div role="img" aria-label="3 out of 5" class="flex gap-0.5">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 fill-amber-400 text-amber-400" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 fill-amber-400 text-amber-400" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 fill-amber-400 text-amber-400" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 text-muted-foreground/30" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 text-muted-foreground/30" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
        </div>
        <span class="text-sm font-medium">Sam K.</span>
        <time dateTime="2026-06-11" class="ms-auto text-xs text-muted-foreground">11 June 2026</time>
      </div>
      <h3 data-stress-ignore="true" class="mt-2.5 font-semibold">Nice but the colour is off</h3>
      <p id="_R_0_-4-body" class="mt-1.5 text-sm leading-relaxed text-muted-foreground ">The stone reads much warmer in person than on screen — closer to a light camel. Lovely sweater, just not the colour I was picturing.</p>
      <div class="mt-3 flex flex-wrap items-center gap-4">
        <button type="button" aria-pressed="false" class="inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs transition-colors border-border/60 text-muted-foreground hover:text-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-up h-3.5 w-3.5" aria-hidden="true"><path d="M7 10v12"></path><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"></path></svg>Helpful<span class="tabular-nums">21</span></button>
      </div>
    </li>
    <li class="py-6">
      <div class="flex flex-wrap items-center gap-x-3 gap-y-1.5">
        <div role="img" aria-label="4 out of 5" class="flex gap-0.5">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 fill-amber-400 text-amber-400" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 fill-amber-400 text-amber-400" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 fill-amber-400 text-amber-400" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 fill-amber-400 text-amber-400" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 text-muted-foreground/30" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
        </div>
        <span class="text-sm font-medium">Devin R.</span>
        <span class="inline-flex items-center gap-1 text-xs font-medium text-emerald-600 dark:text-emerald-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check h-3.5 w-3.5" aria-hidden="true"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"></path><path d="m9 12 2 2 4-4"></path></svg>Verified purchase</span>
        <time dateTime="2026-07-14" class="ms-auto text-xs text-muted-foreground">14 July 2026</time>
      </div>
      <h3 data-stress-ignore="true" class="mt-2.5 font-semibold">Great, but runs slightly long</h3>
      <p id="_R_0_-2-body" class="mt-1.5 text-sm leading-relaxed text-muted-foreground ">No complaints about the material at all. The body is about an inch longer than I expected, which is fine untucked but worth knowing if you are between sizes.</p>
      <div class="mt-3 flex flex-wrap items-center gap-4">
        <span class="rounded-full bg-muted px-2.5 py-1 text-xs text-muted-foreground">Bought L · usually L</span>
        <button type="button" aria-pressed="false" class="inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs transition-colors border-border/60 text-muted-foreground hover:text-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-up h-3.5 w-3.5" aria-hidden="true"><path d="M7 10v12"></path><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"></path></svg>Helpful<span class="tabular-nums">12</span></button>
      </div>
    </li>
    <li class="py-6">
      <div class="flex flex-wrap items-center gap-x-3 gap-y-1.5">
        <div role="img" aria-label="5 out of 5" class="flex gap-0.5">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 fill-amber-400 text-amber-400" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 fill-amber-400 text-amber-400" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 fill-amber-400 text-amber-400" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 fill-amber-400 text-amber-400" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-3.5 w-3.5 fill-amber-400 text-amber-400" aria-hidden="true">
            <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
          </svg>
        </div>
        <span class="text-sm font-medium">Priya M.</span>
        <span class="inline-flex items-center gap-1 text-xs font-medium text-emerald-600 dark:text-emerald-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check h-3.5 w-3.5" aria-hidden="true"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"></path><path d="m9 12 2 2 4-4"></path></svg>Verified purchase</span>
        <time dateTime="2026-06-30" class="ms-auto text-xs text-muted-foreground">30 June 2026</time>
      </div>
      <h3 data-stress-ignore="true" class="mt-2.5 font-semibold">Third one I have bought</h3>
      <p id="_R_0_-3-body" class="mt-1.5 text-sm leading-relaxed text-muted-foreground ">Two in stone, one in navy. They hold their shape better than anything else I own at twice the price.</p>
      <div class="mt-3 flex flex-wrap items-center gap-4">
        <button type="button" aria-pressed="false" class="inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs transition-colors border-border/60 text-muted-foreground hover:text-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-up h-3.5 w-3.5" aria-hidden="true"><path d="M7 10v12"></path><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"></path></svg>Helpful<span class="tabular-nums">8</span></button>
      </div>
    </li>
  </ul>
</section>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • This block is interactive in React — toggles, menus or form state. None of that survives here; the markup is the closed/default state and the handlers are gone. Re-wire them in your own framework.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

Used in these pages

Want the whole screen instead of this one section? Open a page and copy it entire.

6 more blocks in Orders & Reviews

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse Orders & Reviews

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More Orders & Reviews blocks

View category
Open the full page for this block

Order Confirmation

The thank-you screen built to prevent two support emails: a selectable order number and a delivery date range rather than "soon".

Orders & Reviews212 lines1 dep

Order History

Past orders with stacked item thumbnails and worded statuses — people recognise what they bought long before the order number.

Orders & Reviews195 lines1 dep
Open the full page for this block

Order Tracking Timeline

The fortnight between buying and reviewing, answered properly: an arrival range rather than a date, exceptions above the timeline, and a copyable carrier reference.

Orders & Reviews258 lines1 dep
Open the full page for this block

Write a Review

The form that produces the reviews the other blocks display — a real radio group for stars, and a prompt that changes with the rating because one star and five need different questions.

Orders & Reviews249 lines1 dep
Open the full page for this block

Order Lookup Form

Order tracking for the guest who never made an account, which is most of them.

Orders & Reviews176 linesNo deps
Open the full page for this block

Review Distribution Band

The rating summary with the numbers that make an average trustworthy — the count, the spread and the verified share.

Orders & Reviews125 linesNo deps