Rating

Lets users rate items using stars in a simple, interactive way. Provides immediate visual feedback and supports partial or full selections.

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

const value = ref(0)
</script>

<template>
  <Rating v-model="value" label="Rate this article" />
</template>

Sizes

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

<template>
  <div class="flex flex-col gap-4 items-start">
    <Rating size="sm" :model-value="3" label="Small" />
    <Rating size="md" :model-value="3" label="Medium" />
    <Rating size="lg" :model-value="3" label="Large" />
    <Rating size="xl" :model-value="3" label="Extra large" />
  </div>
</template>

Half stars

Set step="0.5" to allow half-star ratings. The control switches its ARIA role to slider so screen readers can announce non-integer values.

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

const value = ref(3.5)
</script>

<template>
  <Rating v-model="value" :step="0.5" label="Half-star rating" />
</template>

Clearing

Clicking the currently-selected star (or pressing 0) clears the rating to 0. To opt out, bind manually and drop the 0 update:

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

Custom icon

icon accepts either a Vue component — typically an auto-imported lucide icon (import LucideHeart from '~icons/lucide/heart') — or a string class name (e.g. "lucide-zap").

vue
<script setup lang="ts">
import { ref } from 'vue'
import { Rating } from 'frappe-ui'
import LucideHeart from '~icons/lucide/heart'
import LucideFlame from '~icons/lucide/flame'

const heart = ref(3.5)
const flame = ref(3)
const zap = ref(3)
</script>

<template>
  <div class="flex flex-col gap-4 items-center">
    <Rating v-model="heart" :step="0.5" label="Hearts (red, half-step)">
      <template #icon="{ state }">
        <LucideHeart
          fill="currentColor"
          class="size-5"
          :class="{
            'text-red-500': state === 'filled',
            'text-red-200': state === 'preview',
            'text-red-300': state === 'removing',
            'text-gray-300 dark:text-gray-600': state === 'empty',
          }"
        />
      </template>
    </Rating>
    <Rating v-model="flame" :icon="LucideFlame" label="Flames (component)" />
    <Rating v-model="zap" icon="lucide-zap" label="Zap (lucide-* string)" />
  </div>
</template>

Custom icon slot

For per-index content (emojis, mixed icons) or full control over color and styling, use the #icon slot. It's called once per star and stamped into both half-spans so half-step clipping still works.

The slot receives { index, side, state, leftState, rightState, value, previewValue, max }. Drive your style off state (filled | preview | removing | empty).

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-2xl 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-2xl leading-none"
          :class="
            index === (previewValue ?? value) ? '' : 'grayscale opacity-50'
          "
        >
          {{ scores[index - 1] }}
        </span>
      </template>
    </Rating>
  </div>
</template>

Labeling

How would you rate the experience?

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

const value = ref(0)
const required = ref(true)
const showError = ref(false)

const error = computed(() =>
  showError.value ? 'Please rate before submitting.' : '',
)
</script>

<template>
  <div class="flex gap-8 items-center">
    <Rating
      v-model="value"
      label="Quality"
      description="How would you rate the experience?"
      :error="error"
      :required="required"
    />
    <div
      class="flex flex-col gap-2 items-start border-l border-outline-gray-2 pl-6"
    >
      <Checkbox v-model="required" label="required" />
      <Checkbox v-model="showError" label="show error" />
    </div>
  </div>
</template>

States

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

<template>
  <div class="flex flex-col gap-4 items-start">
    <Rating label="Default" :model-value="3" />
    <Rating label="Required" required :model-value="3" />
    <Rating label="Disabled" disabled :model-value="3" />
    <Rating
      label="With error"
      error="Please rate before submitting."
      :model-value="3"
    />
  </div>
</template>

Keyboard

ModeKeysAction
Radiogroup (step="1") / / / Move focus and select adjacent star
Home / EndSelect first / last star
Space / EnterSelect the focused star
19Set the rating to that value
Slider (step="0.5") / / / Decrement / increment by step
PageUp / PageDownIncrement / decrement by one full star
Home / EndSet to 0 / max
09Set the rating to that integer

Customization

Each star exposes data-attribute hooks for styling:

  • Root: data-slot="control", data-size, data-disabled, data-state="valid|invalid".
  • Star: data-slot="star", data-index, data-state="filled|preview|removing|empty".
  • Half-star fill: each star renders two half-spans with their own data-state for half-step granularity.

Deprecated props

  • rating_from is kept as an alias for max.
  • readonly is kept as an alias for disabled.

Both fire a one-time dev-mode warnDeprecated warning.

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

  /**
   * Number of stars to render.
   * @deprecated Use `max` instead.
   */
  rating_from?: number

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

  /**
   * If true, disables interaction.
   * @deprecated Use `disabled` instead.
   */
  readonly?: boolean

  /**
   * Icon to render for each star. Accepts a Vue component (e.g. an auto-imported
   * lucide icon: `import Heart from '~icons/lucide/heart'`).
   * The component receives `fill="currentColor"` so closed-path SVGs render filled.
   * Defaults to a filled lucide-star.
   */
  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`.

rating_from

Deprecated — Use `max` instead.

disabled
= false
boolean

If true, disables interaction.

readonly

Deprecated — Use `disabled` instead.

icon
= LucideStar
string | Component

Icon to render for each star. Accepts a Vue component (e.g. an auto-imported lucide icon: `import Heart from '~icons/lucide/heart'`). The component receives `fill="currentColor"` so closed-path SVGs render filled. Defaults to a filled lucide-star.

size
= "md"
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.

error
string | FrappeUIError

Error message rendered below the input. When set, the control receives `aria-invalid="true"` and `data-state="invalid"`. May be either a string or an `Error` object whose `messages?: string[]` is rendered as stacked lines (with `Error.message` as the fallback).

required
boolean

Marks the field as required. Renders an asterisk next to the label and forwards `required` / `aria-required` to the underlying control.

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.