# Reviews

> A React reviews section for products and sellers: average, stars, a 5 to 1 filter breakdown, and sortable reviews with photos and replies.

- Type: React component (Commerce)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add reviews`
- Docs: https://wingo-ui.com/components/reviews

## Interaction

The summary shows the average, its stars and how many reviews there are; the five bars grow in one after another the first time they scroll into view. Tap or click a bar to see only that many stars (it stays highlighted, the other bars dim), tap it again to see all; the chips above the list do the same and add "With photos", and the list reflows with the reviews sliding into place. Sort opens a menu on a laptop and a bottom sheet on a phone. Long reviews stop after a few lines with "Show more", which springs the rest open. Tap a photo to open it full screen (swipe between photos, pinch or double tap to zoom, swipe down or press Escape to close, and focus returns to the photo). "Helpful" votes at once and rolls back if saving fails; the count moves by one with it. On a wide container the summary sits in a sticky column beside the list; below that it stacks above. Keyboard: Tab through the bars, the sort, the chips (arrow keys move between chips), each review's photos, Show more, Helpful and Report; Space or Enter toggles a bar, a chip or Helpful and opens a photo; in the photo viewer Left and Right change the photo and Escape closes it; in the sort menu the arrow keys pick an order and Enter confirms. Every bar, chip and button has a 44px target on touch.

## Installation

```bash
npx wingo-ui@latest add reviews
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/reviews` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/reviews.tsx`

npm dependencies: `npm i lucide-react motion`

Wingo UI dependencies (installed automatically):

- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Collapsible](https://wingo-ui.com/components/collapsible) ([Markdown](https://wingo-ui.com/components/collapsible.md))
- [Empty State](https://wingo-ui.com/components/empty-state) ([Markdown](https://wingo-ui.com/components/empty-state.md))
- [Favorite button](https://wingo-ui.com/components/favorite-button) ([Markdown](https://wingo-ui.com/components/favorite-button.md))
- [Image Lightbox](https://wingo-ui.com/components/image-lightbox) ([Markdown](https://wingo-ui.com/components/image-lightbox.md))
- [Navigation](https://wingo-ui.com/components/navigation) ([Markdown](https://wingo-ui.com/components/navigation.md))
- [Pagination](https://wingo-ui.com/components/pagination) ([Markdown](https://wingo-ui.com/components/pagination.md))
- [Progress](https://wingo-ui.com/components/progress) ([Markdown](https://wingo-ui.com/components/progress.md))
- [Rating](https://wingo-ui.com/components/rating) ([Markdown](https://wingo-ui.com/components/rating.md))
- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [Skeleton](https://wingo-ui.com/components/skeleton) ([Markdown](https://wingo-ui.com/components/skeleton.md))
- [Tag](https://wingo-ui.com/components/tag) ([Markdown](https://wingo-ui.com/components/tag.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [Relative time](https://wingo-ui.com/components/relative-time) ([Markdown](https://wingo-ui.com/components/relative-time.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { Reviews, type ReviewData, type ReviewsLabels } from "@/components/ui/reviews"

const labels: Partial<ReviewsLabels> = {
  title: "Customer reviews",
  write: "Review this product",
}

const reviews: ReviewData[] = [
  {
    id: "r1",
    author: { name: "Julia Davis", avatar: "/demo/ioana-dumitrescu.jpg" },
    rating: 5,
    title: "Exactly what I wanted",
    body: "Fast delivery with UPS, and the bike arrived almost assembled.",
    date: "2026-09-22T10:15:00",
    photos: ["/demo/bicycle.jpg"],
    verified: true,
    helpfulCount: 12,
  },
]

export function ProductReviews() {
  return (
    <Reviews
      reviews={reviews}
      summary={{ average: 4.6, count: 126, distribution: [92, 21, 7, 3, 3] }}
      tone="warning"
      labels={labels}
      onHelpfulChange={async (id, helpful) => {
        const res = await fetch(`/api/reviews/${id}/helpful`, { method: helpful ? "POST" : "DELETE" })
        if (!res.ok) throw new Error("vote failed")
      }}
      onWriteReview={() => {}}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `reviews?` | `ReviewData[]` | `"[]"` | The loaded page of reviews (empty shows the empty state): { id, author: { name, avatar?, href? }, rating, title?, body?, date, photos?, verified?, helpfulCount?, helpful?, reply?: { author, body, date? }, meta?, context? }. |
| `summary?` | `ReviewsSummaryData` | `"summarizeRatings(reviews)"` | { average, count, distribution: [n5, n4, n3, n2, n1], aspects?, photoCount? }. Pass the server's numbers when the list is paged; the bars always use count for their proportions and the average shows one decimal. |
| `layout?` | `"split" \| "stacked"` | `"split"` | split: on a container of 768px or more the summary becomes a sticky column beside the list. stacked: the summary sits above the list, with the average beside the bars once there is room. |
| `showSummary?` | `boolean` | `true` | The average, the stars, the count and the breakdown. |
| `showDistribution?` | `boolean` | `true` | The 5 to 1 star bars in the summary. |
| `filterable?` | `boolean` | `true` | The bars become toggle buttons and the chips row appears (All, With photos, one chip per star). |
| `sortable?` | `boolean` | `true` | The sort select above the list (a bottom sheet on phones). |
| `defaultSort?` | `"relevant" \| "newest" \| "highest" \| "lowest"` | `"relevant"` | The starting order. relevant keeps the order the reviews came in; the others sort the loaded reviews (ties newest first). |
| `sort?` | `ReviewsSort` |  | Controlled sort. |
| `onSortChange?` | `(sort: ReviewsSort) => void` |  | Called when a person picks an order. When given, the list is not sorted locally: reload it in the new order. |
| `filter?` | `ReviewsFilter` |  | Controlled filter, { rating?: 1 to 5; withPhotos?: boolean }. |
| `defaultFilter?` | `ReviewsFilter` | `"{}"` | The starting filter. Filters apply to the loaded reviews; a caller that reloads per filter gets the same result. |
| `onFilterChange?` | `(filter: ReviewsFilter) => void` |  | Called when a bar or a chip changes the filter. |
| `onHelpfulChange?` | `((id: string, helpful: boolean) => void \| Promise<unknown>)` |  | Shows the Helpful vote on every review. It flips at once; return a promise to keep it pending, and a rejection rolls it back and announces labels.helpfulError. |
| `onReport?` | `(id: string) => void` |  | Shows a Report button on every review. |
| `onWriteReview?` | `() => void` |  | Shows "Write a review" in the summary and in the empty state. |
| `maxLines?` | `number` | `4` | Lines of a review's text before "Show more". The toggle only appears when the text is longer. |
| `photoLightbox?` | `boolean` | `true` | Review photos are buttons that open a full-screen viewer (zoom, swipe, swipe down to close). |
| `loading?` | `boolean` | `false` | First load: a skeleton summary and skeleton reviews in the real shape. |
| `skeletonCount?` | `number` | `3` | Skeleton reviews while loading. |
| `hasMore?` | `boolean` | `false` | Shows a full-width "Show more reviews" button with "Showing 8 of 126" under the list. |
| `onLoadMore?` | `(() => void \| Promise<unknown>)` |  | Loads the next page. A returned promise shows the loading state until it settles. |
| `loadingMore?` | `boolean` | `false` | The next page is loading (for callers that do not return a promise). Shows with hasMore. |
| `emptyState?` | `ReactNode` |  | Replaces the default empty state ("No reviews yet" with a Write a review action). |
| `error?` | `ReactNode` |  | Shows an error state with the message in place of the list. |
| `onRetry?` | `() => void` |  | Adds a Try again button to the error state. |
| `size?` | `"sm" \| "md"` | `"md"` | md for a product page; sm for sheets, sidebars and seller cards (a 32px average, smaller stars, 56px photos). |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The stars, the bars and a voted Helpful chip. neutral is black / white; amber stars are tone="warning". |
| `color?` | `string` | `""` | Any CSS color for the stars and bars; replaces the tone. |
| `now?` | `number \| null` | `"useNow()"` | The reference time in ms for "2 days ago"; older than 28 days prints the date. null prints dates only. A timestamp, so it stays out of the playground. |
| `linkComponent?` | `(props: LinkComponentProps) => ReactNode` |  | The router link for author.href (pass next/link's Link). Defaults to the one given to LinkProvider (lib/navigation), else a plain <a>. |
| `locale?` | `string` | `""` | Numbers and dates ("4.6", "126", "9/22/2026"). Empty uses the app's or the browser's locale. |
| `labels?` | `Partial<ReviewsLabels>` |  | Every string, English by default: title, average(value), count(n, formatted), stars(n), all, withPhotos(n, formatted), filters, distribution, aspects, sort, sortOptions, verified, helpful, helpfulCount, helpfulError, report, reply, showMore, showLess, write, empty, emptyDescription, emptyFiltered, emptyFilteredDescription, showAll, errorTitle, retry, loadMore, loadingMore, remaining, showing, photos, photo(i, n), photoError. |
| `lightboxLabels?` | `Partial<ImageLightboxLabels>` |  | Strings of the full-screen photo viewer (close, next, previous, zoom…). |
| `classNames?` | `Partial<Record<"summary" \| "distribution" \| "bar" \| "toolbar" \| "list" \| "review" \| "photos" \| "reply" \| "footer", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra classes for the root, merged last. |

## Parts

### ReviewsSummary (component)

The summary on its own (a seller card, a product header): summary, layout, size, tone, color, showDistribution, filterable, selectedRating, onSelectedRatingChange, onWriteReview, animated (default true: the bars grow in on first view; false shows them filled), locale, labels, classNames. Props: ReviewsSummaryProps.

### ReviewItem (component)

One review as an <article>: review, size, tone, color, maxLines, photoLightbox, onHelpfulChange, onReport, now, linkComponent, locale, labels, lightboxLabels, classNames. Props: ReviewItemProps.

### summarizeRatings (function)

{ average, count, distribution: [n5, n4, n3, n2, n1] } from a list; ratings round to the nearest star. Signature: (reviews: { rating: number }[]) => ReviewsSummaryData.

### DEFAULT_REVIEWS_LABELS (constant)

The English copy of the summary, filters, sort menu, review items and empty states. Spread it into labels when you translate only a few keys.

### ReviewData (type)

One review: id, author { name, avatar?, href? }, rating, title, body, date, photos, verified, helpfulCount, helpful, reply { author, body, date? }, meta and context.

## Examples

- **Stacked on a phone**: The summary above the list, bars full width, chips that scroll sideways and a sort sheet.
- **Seller reviews**: Aspect scores under the bars and what each buyer bought, as context on every review.
- **Loading, then content**: Skeletons in the real shape, then the reviews fade in.
- **Empty**: No reviews yet, with the action that writes the first one.
- **Filtered**: Filtered to 1 star or to reviews with photos. The filter is controlled; bars and chips follow it.
- **Small, inside a drawer**: size="sm" in a sheet on phones and a side panel on a laptop.
- **Error and retry**: A failed load shows the message and a retry that brings the list back.
- **Summary with a custom color**: ReviewsSummary on its own, stars and bars in any CSS color.

## See also

- [Rating](https://wingo-ui.com/components/rating) ([Markdown](https://wingo-ui.com/components/rating.md))
- [Favorite button](https://wingo-ui.com/components/favorite-button) ([Markdown](https://wingo-ui.com/components/favorite-button.md))
- [Image Lightbox](https://wingo-ui.com/components/image-lightbox) ([Markdown](https://wingo-ui.com/components/image-lightbox.md))
- [Comment Thread](https://wingo-ui.com/components/comment-thread) ([Markdown](https://wingo-ui.com/components/comment-thread.md))
- [Listing card](https://wingo-ui.com/components/listing-card) ([Markdown](https://wingo-ui.com/components/listing-card.md))
- [Product gallery](https://wingo-ui.com/components/product-gallery) ([Markdown](https://wingo-ui.com/components/product-gallery.md))

---

Canonical: https://wingo-ui.com/components/reviews
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
