Rating ​

A row of stars for picking a score, such as 4 out of 5.

max
step
size
<Rating
  v-model="value"
/>

Examples ​

Product review ​

A required rating with a description and an error. Submit the form without a rating to see the error.

Click a selected star again to clear it.

vue
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Button, Rating, Textarea } from 'frappe-ui'

const rating = ref(0)
const review = ref('')
const submitted = ref(false)

const error = computed(() =>
  submitted.value && rating.value === 0
    ? 'Pick a rating to post your review.'
    : '',
)
</script>

<template>
  <form
    class="flex w-full max-w-sm flex-col gap-4"
    @submit.prevent="submitted = true"
  >
    <Rating
      v-model="rating"
      label="Overall rating"
      description="Click a selected star again to clear it."
      size="md"
      required
      :error="error"
    />
    <Textarea
      v-model="review"
      label="Your review"
      placeholder="What did you like or dislike?"
    />
    <Button variant="solid" type="submit" class="self-start">
      Post review
    </Button>
  </form>
</template>

Score with half stars ​

step="0.5" lets people pick half a star. Click the left half of a star for the half value.

3.5 / 5
vue
<script setup lang="ts">
import { ref } from 'vue'
import { Rating } from 'frappe-ui'

const score = ref(3.5)
</script>

<template>
  <div class="flex items-end gap-3">
    <Rating v-model="score" :step="0.5" size="lg" label="Your score" />
    <span class="flex h-6 items-center text-base text-ink-gray-7 tabular-nums">
      {{ score.toFixed(1) }} / 5
    </span>
  </div>
</template>

Spice level ​

icon replaces the star with any class icon, here lucide-flame. max sets the number of icons.

Medium
vue
<script setup lang="ts">
import { ref } from 'vue'
import { Rating } from 'frappe-ui'

const spice = ref(2)
const levels = ['Not rated', 'Mild', 'Medium', 'Hot']
</script>

<template>
  <div class="flex items-end gap-3">
    <Rating
      v-model="spice"
      icon="lucide-flame"
      :max="3"
      size="md"
      label="Spice level"
    />
    <span class="flex h-5 items-center text-base text-ink-gray-7">{{
      levels[spice]
    }}</span>
  </div>
</template>

Mood check-in ​

The #icon slot draws a different emoji at each position. The slot's index, value and previewValue pick which one stands out.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { Rating } from 'frappe-ui'

const mood = ref(3)
const moods = ['😞', '🙁', '😐', '🙂', '😊']

const score = ref(2)
const scores = ['💩', '👎', '👌', '👍', '🔥']
</script>

<template>
  <div class="flex flex-col gap-4 items-center">
    <Rating v-model="mood" label="How was your day?" size="lg">
      <template #icon="{ index, value, previewValue }">
        <span
          class="text-3xl leading-none transition-transform"
          :class="
            index === (previewValue ?? value) ? 'scale-110' : 'opacity-40'
          "
        >
          {{ moods[index - 1] }}
        </span>
      </template>
    </Rating>
    <Rating v-model="score" label="Rate this meal" size="lg">
      <template #icon="{ index, value, previewValue }">
        <span
          class="text-3xl leading-none"
          :class="
            index === (previewValue ?? value) ? '' : 'grayscale opacity-50'
          "
        >
          {{ scores[index - 1] }}
        </span>
      </template>
    </Rating>
  </div>
</template>

Behavior ​

Value ​

The value is a number from 0 to max, where 0 means no rating. max defaults to 5. A value outside that range shows as the nearest end, and a value between steps shows rounded to the nearest step.

Clearing ​

Clicking the selected star, or pressing 0, sets the rating to 0. To keep people from clearing it, bind the value yourself and ignore the 0 update:

vue
<Rating
  :model-value="value"
  @update:model-value="(v) => { if (v !== 0) value = v }"
/>

Hover preview ​

While the pointer is over the stars, they show the value a click would pick. Stars that the click would add and stars that it would remove are drawn in lighter shades.

Icon ​

icon takes a class name (icon="lucide-zap"), which renders as a <span> with that class, or a Vue component. A component gets fill="currentColor", so closed shapes render filled.

The default icon is a filled star. The lucide-star class icon draws only the outline, so it cannot replace the default.

Icon slot ​

The #icon slot replaces the icon at each position, for content such as emojis or icons that change from one position to the next. It renders twice per star, once for each half, so half stars still work.

The slot receives { index, side, state, leftState, rightState, value, previewValue, max }. index starts at 1. state is filled, preview, removing or empty, and describes the half being drawn. Use it to set the icon's color. previewValue is the value under the pointer, or null, so previewValue ?? value is the value to highlight.

Size ​

size defaults to sm, like every other input. A value it does not know renders as sm.

Label, description and error ​

label renders above the stars and description below them. error renders below the stars and hides description. It takes a string, an array of strings (one line each), or an Error, the same values as ErrorMessage. An empty string or an empty array means no error. required adds a red asterisk to the label.

The #label slot replaces the label text and the required marker, and receives { required }. A #description slot is not hidden by error. It renders above the error.

Attributes ​

class and style go on the outer element: the wrapper when a label, description or error shows, and the row of stars otherwise. Every other attribute and listener goes on the row of stars.

Accessibility ​

With whole stars, the row is a radiogroup and each star is a radio named "3 of 5". Only the selected star, or the first star when there is no rating, is in the tab order.

KeysAction
ArrowLeft ArrowUp / ArrowRight ArrowDownSelect the previous / next star and move focus to it
Home / EndSelect the first / last star
Space / EnterSelect the focused star
1–9Set the rating to that number
0Clear the rating

With step="0.5", the whole row is one slider. Screen readers announce its value as "3.5 of 5 stars", or "No rating, out of 5 stars" at 0. The slider role does not allow aria-required, so a required rating in this mode is announced only through the hidden "(required)" text in the label.

KeysAction
ArrowLeft ArrowDown / ArrowRight ArrowUpDecrease / increase by half a star
PageDown / PageUpDecrease / increase by one star
Home / EndSet to 0 / max
0–9Set the rating to that number

In both modes, a number above max sets the rating to max.

API Reference ​

Show types
typescript
import { type Component } from 'vue'
import type { InputSize } from '../../composables/inputTypes'
import type { InputLabelingProps } from '../../composables/useInputLabeling'

export interface RatingProps extends InputLabelingProps {
  /** The current rating value (controlled). In star units, `0..max`, in increments of `step`. */
  modelValue?: number

  /** Number of stars to render. Defaults to 5. */
  max?: number

  /**
   * Granularity of the rating value. `1` for whole stars, `0.5` for half stars.
   * Defaults to `1`.
   */
  step?: 1 | 0.5

  /** If true, disables interaction. */
  disabled?: boolean

  /**
   * Icon to render for each star. Accepts a class-icon name
   * (`icon="lucide-heart"`), which renders as a `<span>` carrying that class,
   * or a Vue component. A component receives `fill="currentColor"` so
   * closed-path SVGs render filled.
   *
   * Defaults to an inline filled star. The `lucide-star` class icon is a mask
   * of lucide's outline star, so it cannot draw a solid one.
   */
  icon?: string | Component

  /** Size of the rating component. */
  size?: InputSize
}

export type RatingStarState = 'filled' | 'preview' | 'removing' | 'empty'

export interface RatingIconSlotProps {
  /** 1-based star position. */
  index: number
  /**
   * Which half of the star this invocation renders into. The slot is stamped
   * once per half so clipping works; use `side` to pick the matching state
   * when driving icon color from a slot template under `step === 0.5`.
   */
  side: 'left' | 'right'
  /** State of the half being rendered — equals `leftState` or `rightState` per `side`. */
  state: RatingStarState
  /** State of the left half — equals `rightState` when `step === 1`. */
  leftState: RatingStarState
  /** State of the right half. */
  rightState: RatingStarState
  /** The current saved rating value. */
  value: number
  /**
   * Value currently being previewed via hover, or `null` when not hovering.
   * Combine with `value` for single-select patterns:
   * `previewValue ?? value` gives the index to highlight.
   */
  previewValue: number | null
  /** Total number of stars. */
  max: number
}

export interface RatingEmits {
  /** Fired when the rating value changes. */
  'update:modelValue': [value: number]
}
modelValue
= 0
number

The current rating value (controlled). In star units, `0..max`, in increments of `step`.

max
number

Number of stars to render. Defaults to 5.

step
= 1
1 | 0.5

Granularity of the rating value. `1` for whole stars, `0.5` for half stars. Defaults to `1`.

disabled
= false
boolean

If true, disables interaction.

icon
= RatingStar
string | Component

Icon to render for each star. Accepts a class-icon name (`icon="lucide-heart"`), which renders as a `<span>` carrying that class, or a Vue component. A component receives `fill="currentColor"` so closed-path SVGs render filled. Defaults to an inline filled star. The `lucide-star` class icon is a mask of lucide's outline star, so it cannot draw a solid one.

size
= "sm"
InputSize

Size of the rating component.

label
string

Label rendered above (or beside, for binary controls) the input.

description
string

Helper text rendered below the input. Hidden when `error` is set. A `#description` slot is not: it renders beside the error, and is referenced alongside it.

error
ErrorMessageValue

Error message rendered below the input. When set, the control receives `aria-invalid="true"` and `data-state="invalid"`. Takes a string, an array of strings, or an `Error` whose `messages` are rendered as stacked lines (with `Error.message` as the fallback). This is the same value `ErrorMessage.message` takes. An empty array and an empty string both mean no error.

required
boolean

Marks the field as required. Renders an asterisk next to the label, with `sr-only` text that announces it, and forwards `required` / `aria-required` to the underlying control where the control's role allows it. `data-required` is set either way.

id
string

HTML id of the underlying control. Auto-generated via `useId()` if omitted.

label
{ required: boolean; }

Overrides the rendered label content. Receives `{ required }`.

description
—

Overrides the rendered description content.

icon
RatingIconSlotProps

Overrides the per-star icon. Called once per star and stamped into both half-spans (so half-step clipping still works). Use `state` to color the icon, or `index` to render different content per position (e.g. emojis).

update:modelValue
[value: number]

Fired when the model value changes.

focus
(options?: FocusOptions) => void

Moves focus to the selected star, or to the first star when nothing is selected. That is the star `Tab` reaches. In half-star mode the whole control is one slider, so it focuses the control itself.