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>

Max stars

max controls the number of stars rendered. Defaults to 5.

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

<template>
  <div class="flex flex-col gap-4 items-start">
    <Rating :model-value="3" :max="5" label="5 stars" />
    <Rating :model-value="6" :max="10" label="10 stars" />
  </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. This is the default behavior — there is no clearable prop.

To opt out, use :model-value + @update:model-value and drop the 0 update yourself:

vue
<Rating
  :model-value="value"
  @update:model-value="(v) => { if (v !== 0) value = v }"
/>
vue
<script setup lang="ts">
import { ref } from 'vue'
import { Rating } from 'frappe-ui'

// Default behavior: re-clicking the selected star clears the rating to 0.
const clearable = ref(3)

// Opt-out: bind manually and drop the 0 emitted when the user re-clicks
// the selected star. Any non-zero update is accepted as normal.
const sticky = ref(3)
function onSticky(next: number) {
  if (next === 0) return
  sticky.value = next
}
</script>

<template>
  <div class="w-full flex flex-col gap-4 items-start">
    <Rating
      v-model="clearable"
      label="Default — click the selected star to clear"
    />
    <Rating
      :model-value="sticky"
      label="Disabled — re-clicking keeps the value"
      @update:model-value="onSticky"
    />
  </div>
</template>

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") that is rendered on a <span> for use with the shared Lucide Tailwind utility.

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(4)
const flame = ref(3)
const zap = ref(3)
</script>

<template>
  <div class="w-full flex flex-col gap-4 items-start">
    <Rating v-model="heart" :icon="LucideHeart" label="Hearts (component)" />
    <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 full control over each star — different content per index (emojis, icons), custom colors, or any other per-star 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:

PropTypeNotes
indexnumber1-based star position
side'left' | 'right'Which half-span this invocation renders into
state'filled' | 'preview' | 'removing' | 'empty'The current half's state (matches leftState or rightState per side) — drive your color/style off this
leftState / rightStatesame unionPer-half states for step="0.5", exposed if you need both at once
valuenumberCurrent saved rating
previewValuenumber | nullValue being hovered, or null. Use previewValue ?? value for single-select patterns where hover should preview the selection
maxnumberTotal stars

Per-index content

Render a different element per star — e.g. a mood scale of emojis. Style each one off the state prop with whatever utility classes you like.

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="w-full flex flex-col gap-4 items-start">
    <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>

Custom colors with Tailwind

Map state → Tailwind classes to recolor the control. Use the dark: variant for dark-mode overrides on preview / removing (which need to flip lighter→darker against a dark surface).

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'
import type { RatingStarState } from 'frappe-ui'

const heart = ref(4)
const flame = ref(3)

const redClasses: Record<RatingStarState, string> = {
  filled: 'text-red-500',
  preview: 'text-red-300 dark:text-red-700',
  removing: 'text-red-200 dark:text-red-800',
  empty: 'text-gray-300 dark:text-gray-600',
}

const orangeClasses: Record<RatingStarState, string> = {
  filled: 'text-orange-500',
  preview: 'text-orange-300 dark:text-orange-700',
  removing: 'text-orange-200 dark:text-orange-800',
  empty: 'text-gray-300 dark:text-gray-600',
}
</script>

<template>
  <div class="w-full flex flex-col gap-4 items-start">
    <Rating v-model="heart" label="Hearts">
      <template #icon="{ state }">
        <LucideHeart
          fill="currentColor"
          class="size-5"
          :class="redClasses[state]"
        />
      </template>
    </Rating>
    <Rating v-model="flame" label="Flames">
      <template #icon="{ state }">
        <LucideFlame
          fill="currentColor"
          class="size-5"
          :class="orangeClasses[state]"
        />
      </template>
    </Rating>
  </div>
</template>

The same pattern works with step="0.5" because state reflects the current half, so each half-span picks up the correct color independently.

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

const heart = ref(3.5)

const redClasses: Record<RatingStarState, string> = {
  filled: 'text-red-500',
  preview: 'text-red-300 dark:text-red-700',
  removing: 'text-red-200 dark:text-red-800',
  empty: 'text-gray-300 dark:text-gray-600',
}
</script>

<template>
  <Rating v-model="heart" :step="0.5" label="Half-star hearts">
    <template #icon="{ state }">
      <LucideHeart
        fill="currentColor"
        class="size-5"
        :class="redClasses[state]"
      />
    </template>
  </Rating>
</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-start">
    <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="Read-only" readonly :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 by step
/ 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-readonly, 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.

For color or per-star customization, see Custom icon slot above.

Deprecated rating_from prop

The rating_from prop is kept for backwards compatibility and fires a dev-mode [frappe-ui] Rating.rating_from is deprecated. Use max instead. warning. Use max instead.

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

<!--
  `rating_from` is deprecated. Use `max` instead. Mounting this story
  fires a one-time `[frappe-ui] Rating.rating_from is deprecated`
  warning in the console.
-->

<template>
  <Rating label="Legacy field" :model-value="3" :rating_from="10" />
</template>

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 and makes the rating read-only. */
  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.

readonly
= false
boolean

If true, disables interaction and makes the rating read-only.

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.