Rating
Lets users rate items using stars in a simple, interactive way. Provides immediate visual feedback and supports partial or full selections.
<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
<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.
<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:
<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").
<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).
<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
<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
<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
| Mode | Keys | Action |
|---|---|---|
Radiogroup (step="1") | ← / ↑ / → / ↓ | Move focus and select adjacent star |
Home / End | Select first / last star | |
Space / Enter | Select the focused star | |
1–9 | Set the rating to that value | |
Slider (step="0.5") | ← / ↓ / → / ↑ | Decrement / increment by step |
PageUp / PageDown | Increment / decrement by one full star | |
Home / End | Set to 0 / max | |
0–9 | Set 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-statefor half-step granularity.
Deprecated props
rating_fromis kept as an alias formax.readonlyis kept as an alias fordisabled.
Both fire a one-time dev-mode warnDeprecated warning.
API Reference
Show types
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]
}The current rating value (controlled). In star units, `0..max`, in increments of `step`.
Number of stars to render. Defaults to 5.
Granularity of the rating value. `1` for whole stars, `0.5` for half stars. Defaults to `1`.
Deprecated — Use `max` instead.
If true, disables interaction.
Deprecated — Use `disabled` instead.
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 of the rating component.
Label rendered above (or beside, for binary controls) the input.
Helper text rendered below the input. Hidden when `error` is set.
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).
Marks the field as required. Renders an asterisk next to the label and forwards `required` / `aria-required` to the underlying control.
HTML id of the underlying control. Auto-generated via `useId()` if omitted.
| Slot | Payload |
|---|---|
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). |
Overrides the rendered label content. Receives `{ required }`.
Overrides the rendered description content.
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).
| Event | Payload |
|---|---|
update:modelValue | [value: number] Fired when the model value changes. |
Fired when the model value changes.