---
name: reviews
title: Reviews
url: https://ui.frontic.com/components/sections/reviews.md
tier: sections
install: npx @frontic/ui add reviews
files:
  - components/sections/reviews/Reviews.vue
  - components/sections/reviews/index.ts
  - components/sections/reviews/types.ts
registryDependencies:
  - avatar
  - button
  - card
  - format
  - progress
  - rating
  - separator
  - text
dependencies:
  - "@lucide/vue"
  - reka-ui
---

# Reviews

Review summary card with the average rating, a star distribution, the latest reviews with verified-purchase marks, and read-all and write-a-review actions.

## Install

```sh
npx @frontic/ui add reviews
```

Built from: [Avatar](https://ui.frontic.com/components/ui/avatar.md), [Button](https://ui.frontic.com/components/ui/button.md), [Card](https://ui.frontic.com/components/ui/card.md), [Format](https://ui.frontic.com/lib/format.md), [Progress](https://ui.frontic.com/components/ui/progress.md), [Rating](https://ui.frontic.com/components/ui/rating.md), [Separator](https://ui.frontic.com/components/ui/separator.md), [Text](https://ui.frontic.com/lib/text.md)

npm packages: `@lucide/vue`, `reka-ui`

## Examples

```vue
<script setup lang="ts">
import { Reviews, type ReviewsEntry, type ReviewsShare } from '@/components/sections/reviews'

const avatar = (n: number) => `https://static.frontic.com/cdn-cgi/image/width=128,format=auto/ui/avatars/${n}.jpg`

const distribution: ReviewsShare[] = [
  { stars: 5, count: 151 },
  { stars: 4, count: 38 },
  { stars: 3, count: 12 },
  { stars: 2, count: 7 },
  { stars: 1, count: 4 },
]

const reviews: ReviewsEntry[] = [
  {
    author: 'Marta K.',
    avatar: avatar(8),
    date: '2026-10-04',
    rating: 5,
    content: 'A week of −15 °C and a biting wind on the slopes, and I stayed warm. The hood fits over a helmet.',
    verified: true,
  },
  {
    author: 'Jonas B.',
    avatar: avatar(3),
    date: '2026-09-28',
    rating: 4,
    content: 'Warm without the bulk. Runs a little long in the arms, so I went one size down.',
    verified: true,
  },
]
</script>

<template>
  <div class="mx-auto w-full max-w-md">
    <Reviews description="Frost Escape Parka" :rating="4.6" :total="212" :distribution="distribution" :reviews="reviews" />
  </div>
</template>
```

## API reference

### Reviews

Shows the average rating with its star breakdown and a few reviews.

Styling: `reviews-count`, which extends [`meta`](https://ui.frontic.com/DESIGN.md#meta); `reviews-stars`, which extends [`meta`](https://ui.frontic.com/DESIGN.md#meta); `reviews-verified`, which extends [`note-positive`](https://ui.frontic.com/DESIGN.md#note-positive); [`heading`](https://ui.frontic.com/DESIGN.md#heading); [`meta`](https://ui.frontic.com/DESIGN.md#meta); [`line-title`](https://ui.frontic.com/DESIGN.md#line-title); [`hint`](https://ui.frontic.com/DESIGN.md#hint). Change them there and every part that extends them follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` | `string` | `'Customer reviews'` | Shown as the card's title. |
| `description` | `string` |  | What the reviews are for, such as the product name. |
| `rating` (required) | `number` |  | The average rating, 0 to 5. |
| `total` (required) | `number` |  | How many reviews there are in all. |
| `distribution` | `ReviewsShare[]` | `[]` | How many reviews gave each star rating, five stars first. |
| `reviews` | `ReviewsEntry[]` | `[]` | The reviews listed under the summary. |
| `totalLabel` | `((count: number) => string)`: `(count: number): string` | `count === 1 ? '1 review' : `${count} reviews`` | Shown under the average, saying how many reviews there are. |
| `starsLabel` | `((stars: number) => string)`: `(stars: number): string` | `stars === 1 ? '1 star' : `${stars} stars`` | Screen-reader label of each rating bar, e.g. `4 stars`. |
| `verifiedLabel` | `string` | `'Verified purchase'` | Shown beside the rating of a review from a verified purchase. |
| `readAllLabel` | `((count: number) => string)`: `(count: number): string` | ``Read all ${count}`` | Shown on the button that opens all reviews. |
| `writeLabel` | `string` | `'Write a review'` | Shown on the button that starts a new review. |
| `ratingShape` | `'star' \| 'dot'` | `'star'` | The mark each review's rating is drawn with. |
| `surface` | `'raised' \| 'bare'` | `'raised'` | Bare drops the edge, shadow and inset, for a section placed inside another Card. |
| `locale` | `string` |  | BCP 47 locale the score and dates are formatted in; the ConfigProvider's when left out. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Event | Payload | Description |
| --- | --- | --- |
| `readAll` |  | Fires when the shopper clicks the button to read all reviews. |
| `write` |  | Fires when the shopper clicks the button to write a review. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-surface` | `'raised' \| 'bare'` | Carries the `surface` prop. |

## Exports

```ts
import { type ReviewsEntry, type ReviewsShare } from '@/components/sections/reviews'
```

- `ReviewsEntry`: below
- `ReviewsShare`: below

```ts
interface ReviewsEntry {
  author: string
  /** The author's picture, shown in the avatar. */
  avatar?: string
  /** Shown in the avatar when there is no picture; the author's initials when missing. */
  initials?: string
  /** ISO date, shown in the item's locale. */
  date?: string
  rating: number
  content: string
  verified?: boolean
}

interface ReviewsShare {
  /** The star rating, 1 to 5. */
  stars: number
  count: number
}
```

## Styling

| Part | Recipe |
| --- | --- |
| `stars` | [`reviews-stars`](https://ui.frontic.com/DESIGN.md#reviews-stars) |
| `count` | [`reviews-count`](https://ui.frontic.com/DESIGN.md#reviews-count) |
| `verified` | [`reviews-verified`](https://ui.frontic.com/DESIGN.md#reviews-verified) |
| `heading` | [`heading`](https://ui.frontic.com/DESIGN.md#heading) |
| `meta` | [`meta`](https://ui.frontic.com/DESIGN.md#meta) |
| `line-title` | [`line-title`](https://ui.frontic.com/DESIGN.md#line-title) |
| `hint` | [`hint`](https://ui.frontic.com/DESIGN.md#hint) |

## Source

`components/sections/reviews/Reviews.vue`

```vue
<script lang="ts">
/** Shows the average rating with its star breakdown and a few reviews. */
export default {}
</script>

<script setup lang="ts">
import { BadgeCheckIcon } from '@lucide/vue'

import type { HTMLAttributes } from 'vue'
import type { ReviewsEntry, ReviewsShare } from './types'
import { Avatar, AvatarFallback, AvatarImage } from '@library/components/ui/avatar'
import { Button } from '@library/components/ui/button'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@library/components/ui/card'
import { Progress } from '@library/components/ui/progress'
import { Rating } from '@library/components/ui/rating'
import { Separator } from '@library/components/ui/separator'
import { formatDate, formatRating, percentOf } from '@library/lib/format'
import { useLocale } from 'reka-ui'
import { toRef } from 'vue'
import { initials } from '@library/lib/text'

const props = withDefaults(
  defineProps<{
    /** Shown as the card's title. */
    title?: string
    /** What the reviews are for, such as the product name. */
    description?: string
    /** The average rating, 0 to 5. */
    rating: number
    /** How many reviews there are in all. */
    total: number
    /** How many reviews gave each star rating, five stars first. */
    distribution?: ReviewsShare[]
    /** The reviews listed under the summary. */
    reviews?: ReviewsEntry[]
    /** Shown under the average, saying how many reviews there are. */
    totalLabel?: (count: number) => string
    /** Screen-reader label of each rating bar, e.g. `4 stars`. */
    starsLabel?: (stars: number) => string
    /** Shown beside the rating of a review from a verified purchase. */
    verifiedLabel?: string
    /** Shown on the button that opens all reviews. */
    readAllLabel?: (count: number) => string
    /** Shown on the button that starts a new review. */
    writeLabel?: string
    /** The mark each review's rating is drawn with. */
    ratingShape?: 'dot' | 'star'
    /** Bare drops the edge, shadow and inset, for a section placed inside another Card. */
    surface?: 'raised' | 'bare'
    /** BCP 47 locale the score and dates are formatted in; the ConfigProvider's when left out. */
    locale?: string
    class?: HTMLAttributes['class']
  }>(),
  {
    surface: 'raised',
    title: 'Customer reviews',
    distribution: () => [],
    reviews: () => [],
    totalLabel: (count: number) => (count === 1 ? '1 review' : `${count} reviews`),
    starsLabel: (stars: number) => (stars === 1 ? '1 star' : `${stars} stars`),
    verifiedLabel: 'Verified purchase',
    readAllLabel: (count: number) => `Read all ${count}`,
    writeLabel: 'Write a review',
    ratingShape: 'star',
  },
)
const locale = useLocale(toRef(() => props.locale))
const emit = defineEmits<{
  /** Fires when the shopper clicks the button to read all reviews. */
  readAll: []
  /** Fires when the shopper clicks the button to write a review. */
  write: []
}>()

const share = (count: number) => percentOf(count, props.total)
const initialsOf = (review: ReviewsEntry) => initials(review.author, review.initials)
</script>

<template>
  <Card :surface="surface" :class="['@container', props.class]">
    <CardHeader>
      <CardTitle>{{ title }}</CardTitle>
      <CardDescription v-if="description">{{ description }}</CardDescription>
    </CardHeader>
    <CardContent class="flex flex-col gap-6">
      <div class="flex items-center gap-6">
        <div class="flex shrink-0 flex-col gap-1">
          <span data-slot="heading" class="fui-heading">{{ formatRating(rating, locale) }}</span>
          <span data-slot="meta" class="fui-meta">{{ totalLabel(total) }}</span>
        </div>
        <ul v-if="distribution.length" class="flex flex-1 flex-col gap-1.5">
          <li v-for="row in distribution" :key="row.stars" class="flex items-center gap-2">
            <span data-slot="reviews-stars" class="fui-reviews-stars flex shrink-0 items-center tabular-nums">
              {{ row.stars }}
            </span>
            <Progress :model-value="share(row.count)" :aria-label="starsLabel(row.stars)" class="flex-1" />
            <span data-slot="reviews-count" class="fui-reviews-count shrink-0 tabular-nums">{{ row.count }}</span>
          </li>
        </ul>
      </div>
      <template v-if="reviews.length">
        <Separator />
        <div class="flex flex-col gap-5">
          <article v-for="(review, index) in reviews" :key="index" class="flex gap-3">
            <Avatar size="sm">
              <AvatarImage v-if="review.avatar" :src="review.avatar" :alt="review.author" />
              <AvatarFallback>{{ initialsOf(review) }}</AvatarFallback>
            </Avatar>
            <div class="flex min-w-0 flex-1 flex-col gap-1">
              <div class="flex items-center justify-between gap-2">
                <span data-slot="line-title" class="fui-line-title">{{ review.author }}</span>
                <time v-if="review.date" data-slot="meta" class="fui-meta" :datetime="review.date">{{ formatDate(review.date, { year: 'numeric', month: 'short', day: 'numeric' }, locale) }}</time>
              </div>
              <div class="flex flex-wrap items-center gap-x-2 gap-y-1">
                <Rating :rating="review.rating" size="sm" :shape="ratingShape" />
                <span v-if="review.verified" data-slot="reviews-verified" class="fui-reviews-verified flex items-center">
                  <BadgeCheckIcon data-icon />
                  {{ verifiedLabel }}
                </span>
              </div>
              <p data-slot="hint" class="fui-hint">{{ review.content }}</p>
            </div>
          </article>
        </div>
      </template>
    </CardContent>
    <CardFooter class="flex-col-reverse gap-2 @sm:flex-row">
      <Button variant="outline" color="neutral" class="w-full @sm:w-auto @sm:flex-1" @click="emit('readAll')">{{ readAllLabel(total) }}</Button>
      <Button class="w-full @sm:w-auto @sm:flex-1" @click="emit('write')">{{ writeLabel }}</Button>
    </CardFooter>
  </Card>
</template>
```

`components/sections/reviews/types.ts`

```ts
export interface ReviewsShare {
  /** The star rating, 1 to 5. */
  stars: number
  count: number
}

export interface ReviewsEntry {
  author: string
  /** The author's picture, shown in the avatar. */
  avatar?: string
  /** Shown in the avatar when there is no picture; the author's initials when missing. */
  initials?: string
  /** ISO date, shown in the item's locale. */
  date?: string
  rating: number
  content: string
  verified?: boolean
}
```
