---
name: rating
title: Rating
url: https://ui.frontic.com/components/ui/rating.md
tier: ui
install: npx @frontic/ui add rating
files:
  - components/ui/rating/Rating.vue
  - components/ui/rating/RatingIcon.vue
  - components/ui/rating/RatingMeta.vue
  - components/ui/rating/index.ts
registryDependencies:
  - format
dependencies:
  - "@lucide/vue"
  - class-variance-authority
  - reka-ui
---

# Rating

Shows a rating as stars or dots in the brand's rating colour, with an optional score and review count, at three sizes.

## Install

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

Built from: [Format](https://ui.frontic.com/lib/format.md)

Used by: [Filter](https://ui.frontic.com/components/ui/filter.md), [Rating Input](https://ui.frontic.com/components/ui/rating-input.md), [Product Card Basic](https://ui.frontic.com/components/sections/product-card-basic.md), [Product Card Gallery](https://ui.frontic.com/components/sections/product-card-gallery.md), [Product Card Horizontal](https://ui.frontic.com/components/sections/product-card-horizontal.md), [Product Card Hover](https://ui.frontic.com/components/sections/product-card-hover.md), [Product Card Minimal](https://ui.frontic.com/components/sections/product-card-minimal.md), [Product Options](https://ui.frontic.com/components/sections/product-options.md), [Review Cards](https://ui.frontic.com/components/sections/review-cards.md), [Review List](https://ui.frontic.com/components/sections/review-list.md), [Reviews](https://ui.frontic.com/components/sections/reviews.md)

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

## Examples

```vue
<script setup lang="ts">
import { Rating } from '@/components/ui/rating'
</script>

<template>
  <Rating :rating="4.6" :count="128" show-value />
</template>
```

## Anatomy

```vue
<Rating>
  <RatingIcon />
  <RatingMeta />
</Rating>
```

## API reference

### Rating

Shows a rating as filled and empty marks, with an optional value and review count; the brand's rating colour fills the marks. Renders a `<div>`.

`data-slot="rating"` · `Rating.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `size` | `RatingSize`: `'md' \| 'sm' \| 'lg'` | `'md'` | The size of each mark. |
| `shape` | `'star' \| 'dot'` | `'star'` | Draws the rating as dots or stars. |
| `rating` (required) | `number` |  | The score to show; the mark it reaches into fills by its fraction. |
| `max` | `number` | `5` | The number of marks, and the top of the scale. |
| `count` | `number` |  | The number of reviews, shown in brackets after the score. |
| `showValue` | `boolean` | `false` | A filled #meta slot takes precedence over showValue. |
| `label` | `((score: number, max: number, count?: number \| undefined) => string)`: `(score: number, max: number, count?: number \| undefined): string` | ``${score} out of ${max}${count ? `, ${count} ${count === 1 ? 'review' : 'reviews'}` : ''}`` | Builds the screen-reader text from the score, the top of the scale and the review count, e.g. `4 out of 5`. The score is the rating to one decimal; the count comes only when showValue prints it. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `meta` | `{ rating: number; max: number; count: number \| undefined; }` | Replaces the score and review count after the marks; gets the rating, the top of the scale and the review count. |

### RatingIcon

Shows one mark of the rating, either filled or empty.

`data-slot="rating-icon"` · `RatingIcon.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `size` | `RatingSize`: `'md' \| 'sm' \| 'lg'` | `'md'` | The size of the mark. |
| `shape` | `RatingShape`: `'star' \| 'dot'` | `'star'` | Draws the mark as a dot or a star. |
| `empty` | `boolean` | `false` | Draws the mark unfilled, for the part of the rating it does not reach. |

### RatingMeta

Shows the numeric rating and the number of reviews behind it. Renders a `<div>`.

`data-slot="rating-meta"` · `RatingMeta.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `rating` (required) | `number` |  | The score, shown as `rating/max`. |
| `max` (required) | `number` |  | The top of the scale, shown after the score. |
| `count` | `number` |  | The number of reviews, shown in brackets; hidden when 0 or left out. |
| `locale` | `string` |  | BCP 47 locale the score is formatted in; the ConfigProvider's when left out. |

## Exports

```ts
import { type RatingSize, type RatingShape } from '@/components/ui/rating'
```

- `RatingSize`: `'sm' \| 'md' \| 'lg'`
- `RatingShape`: `'dot' \| 'star'`

## Styling

| Part | Recipe |
| --- | --- |
| `default` | [`rating`](https://ui.frontic.com/DESIGN.md#rating) |
| `stars` | [`rating-stars`](https://ui.frontic.com/DESIGN.md#rating-stars) |
| `meta` | [`rating-meta`](https://ui.frontic.com/DESIGN.md#rating-meta) |
| `value` | [`rating-value`](https://ui.frontic.com/DESIGN.md#rating-value) |
| `icon` | [`rating-icon`](https://ui.frontic.com/DESIGN.md#rating-icon) |
| `dot` | [`rating-dot`](https://ui.frontic.com/DESIGN.md#rating-dot) |
| `count` | [`rating-count`](https://ui.frontic.com/DESIGN.md#rating-count) |

## Source

`components/ui/rating/Rating.vue`

```vue
<script lang="ts">
/** Shows a rating as filled and empty marks, with an optional value and review count; the brand's rating colour fills the marks. */
export default {}
</script>

<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cva } from 'class-variance-authority'
import { computed } from 'vue'
import { type RatingSize, RatingIcon, RatingMeta } from '.'

const props = withDefaults(
  defineProps<{
    class?: HTMLAttributes['class']
    /** The size of each mark. */
    size?: RatingSize
    /** Draws the rating as dots or stars. */
    shape?: 'dot' | 'star'
    /** The score to show; the mark it reaches into fills by its fraction. */
    rating: number
    /** The number of marks, and the top of the scale. */
    max?: number
    /** The number of reviews, shown in brackets after the score. */
    count?: number
    /** A filled #meta slot takes precedence over showValue. */
    showValue?: boolean
    /** Builds the screen-reader text from the score, the top of the scale and the review count, e.g. `4 out of 5`. The score is the rating to one decimal; the count comes only when showValue prints it. */
    label?: (score: number, max: number, count?: number) => string
  }>(),
  {
    size: 'md',
    shape: 'star',
    max: 5,
    showValue: false,
    label: (score: number, max: number, count?: number) => `${score} out of ${max}${count ? `, ${count} ${count === 1 ? 'review' : 'reviews'}` : ''}`,
  },
)

const score = computed(() => Math.round(Math.max(0, Math.min(props.rating, props.max)) * 10) / 10)
const filledStars = computed(() => Math.floor(score.value))
const fraction = computed(() => Math.round((score.value - filledStars.value) * 100))
const emptyStars = computed(() => props.max - filledStars.value - (fraction.value ? 1 : 0))
const meta = () => !slots.meta && props.showValue
const label = () => (meta() ? props.label(score.value, props.max, props.count) : props.label(score.value, props.max))

const ratingRoot = cva('fui-rating')
const stars = cva('fui-rating-stars flex items-center', {
  variants: { shape: { dot: 'fui-rating-stars-dot', star: 'fui-rating-stars-star' } },
})

const slots = defineSlots<{
  /** Replaces the score and review count after the marks; gets the rating, the top of the scale and the review count. */
  meta?: (props: { rating: number; max: number; count: number | undefined }) => unknown
}>()
</script>

<template>
  <div data-slot="rating" :class="[ratingRoot(), props.class]">
    <span data-slot="rating-stars" role="img" :aria-label="label()" :class="stars({ shape: props.shape })">
      <RatingIcon v-for="i in filledStars" :key="`filled-${i}`" aria-hidden="true" :size="props.size" :shape="props.shape" />
      <span v-if="fraction" data-slot="rating-partial" aria-hidden="true" class="relative inline-flex shrink-0">
        <RatingIcon empty :size="props.size" :shape="props.shape" />
        <span data-slot="rating-partial-fill" class="absolute inset-y-0 inset-s-0 flex overflow-hidden" :style="{ width: `${fraction}%` }">
          <RatingIcon :size="props.size" :shape="props.shape" />
        </span>
      </span>
      <RatingIcon v-for="i in emptyStars" :key="`empty-${i}`" aria-hidden="true" empty :size="props.size" :shape="props.shape" />
    </span>
    <span v-if="$slots.meta" data-slot="rating-meta" class="fui-rating-meta flex items-baseline">
      <span data-slot="rating-count" class="fui-rating-count"><slot name="meta" :rating="props.rating" :max="props.max" :count="props.count" /></span>
    </span>
    <RatingMeta v-else-if="meta()" aria-hidden="true" :rating="score" :max="props.max" :count="props.count" />
  </div>
</template>
```

`components/ui/rating/RatingIcon.vue`

```vue
<script lang="ts">
/** Shows one mark of the rating, either filled or empty. */
export default {}
</script>

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

import type { HTMLAttributes } from 'vue'
import { cva } from 'class-variance-authority'
import { computed } from 'vue'
import type { RatingShape, RatingSize } from '.'

const ratingDot = cva('fui-rating-dot', {
  variants: {
    fill: { full: 'fui-rating-dot-full', empty: 'fui-rating-dot-empty' },
    size: { sm: 'fui-rating-dot-sm', md: 'fui-rating-dot-md', lg: 'fui-rating-dot-lg' },
  },
  defaultVariants: { fill: 'full', size: 'md' },
})

const ratingIcon = cva('fui-rating-icon', {
  variants: {
    fill: { full: 'fui-rating-icon-full', empty: 'fui-rating-icon-empty' },
    size: { sm: 'fui-rating-icon-sm', md: 'fui-rating-icon-md', lg: 'fui-rating-icon-lg' },
  },
  defaultVariants: { fill: 'full', size: 'md' },
})

defineOptions({ inheritAttrs: false })

const props = withDefaults(
  defineProps<{
    class?: HTMLAttributes['class']
    /** The size of the mark. */
    size?: RatingSize
    /** Draws the mark as a dot or a star. */
    shape?: RatingShape
    /** Draws the mark unfilled, for the part of the rating it does not reach. */
    empty?: boolean
  }>(),
  { size: 'md', shape: 'star', empty: false },
)

const fill = computed(() => (props.empty ? 'empty' : 'full'))
</script>

<template>
  <slot>
    <span v-if="props.shape === 'dot'" v-bind="$attrs" data-slot="rating-dot" :class="[ratingDot({ fill, size: props.size }), props.class]" />
    <StarIcon v-else v-bind="$attrs" data-slot="rating-icon" :class="[ratingIcon({ fill, size: props.size }), props.class]" />
  </slot>
</template>
```

`components/ui/rating/RatingMeta.vue`

```vue
<script lang="ts">
/** Shows the numeric rating and the number of reviews behind it. */
export default {}
</script>

<script setup lang="ts">
import { formatRating } from '@library/lib/format'
import { useLocale } from 'reka-ui'
import { toRef } from 'vue'
const props = defineProps<{
  /** The score, shown as `rating/max`. */
  rating: number
  /** The top of the scale, shown after the score. */
  max: number
  /** The number of reviews, shown in brackets; hidden when 0 or left out. */
  count?: number
  /** BCP 47 locale the score is formatted in; the ConfigProvider's when left out. */
  locale?: string
}>()
const locale = useLocale(toRef(() => props.locale))
</script>

<template>
  <div data-slot="rating-meta" class="fui-rating-meta">
    <span data-slot="rating-value" class="fui-rating-value">{{ formatRating(props.rating, locale) }}/{{ props.max }}</span>
    <span v-if="props.count" data-slot="rating-count" class="fui-rating-count">({{ props.count }})</span>
  </div>
</template>
```

`components/ui/rating/index.ts`

```ts
export { default as Rating } from './Rating.vue'
export { default as RatingIcon } from './RatingIcon.vue'
export { default as RatingMeta } from './RatingMeta.vue'

export type RatingSize = 'sm' | 'md' | 'lg'
export type RatingShape = 'dot' | 'star'
```
