GrRating

Package: @feugene/granularitycoreGroup: forms

Collect and display a star rating, with half steps and a read-only mode.

Machine-translated from the Russian original, not yet reviewed. Read the original

When to take it

  • the rating is given in one click — a review, the quality of support, the difficulty of a task;
  • the rating is fractionalallowHalf for halves;
  • an explanation is needed beside ittexts labels the steps with words: “Poor”, “Excellent”;
  • the rating is shown rather than givenreadonly for the average rating in a product card.

When to take something else

NeedTake
The scale is numeric and wideGrSlider
An exact number is neededGrNumberInput
There are few options and they are namedGrRadioGroup
Show a share rather than a ratingGrProgressBar

The rating in words

<GrRating v-model="score" :texts="['Awful', 'Poor', 'Fair', 'Good', 'Excellent']" show-text />

texts holds one label per division. The label goes both into the visible text and into aria-valuetext (“4 of 5, good”): that is the whole point of a rating — a screen reader reads the score in words rather than as a bare number. A fractional value is rounded up to its division, and an array shorter than max leaves the upper divisions without a label.

formatText is stronger than texts for the visible text — the consumer may have a wording of their own.

The preview

Hovering shows the rating under the cursor and emits hoverChange; leaving the scale and losing the focus return the model and send hoverChange(null).

The handler hangs on the scale itself rather than on the whole component: the caption lies beside the scale, and a cursor moved onto it has to put the preview out — otherwise it sticks. In readonly and disabled there is no preview at all.

There is no preview from the keyboard: the arrows commit the rating at once, and hoverChange is not emitted there.

The compact look

<GrRating :model-value="3" readonly compact show-text />

compact draws only the filled symbols — for tables and lists, where five stars in every row eat up the width. A half counts as a symbol: “2.5” is drawn with three. For a screen reader the compact mode is no different — the role and the text label are the same.

The modes and accessibility

An interactive scale implements the slider pattern: role="slider", aria-valuemin/max/now/valuetext, the arrows, Home/End. The read-only mode is a role="img" with a text label, so that the rating is read as a single phrase rather than as a set of elements.

disabled dims the scale with the --gr-disabled-fg token rather than with transparency: opacity dilutes colours tuned to AA.

Styling

PointWhat it sets
sizexslg, read from GrConfigProvider
tonethe colour of the fill from the tone scale
--gr-rating-colorthe colour of the fill pointwise, stronger than tone
--gr-rating-void-colorthe colour of an unfilled symbol
icon / the #symbol slota symbol of your own instead of the built-in star

The default symbol is an inline SVG: the icon masks of i-lucide-star give an outline only. A symbol of your own is a Vue component or an icon class from your UnoCSS build (i-lucide-* requires your presetIcons, see “Icons”).

The native form

The name prop renders an input[type="hidden"] with the value of the rating. 0 means “not selected”: the input is not rendered, as with an unchecked radio button.

Playground 14

Loading…

Code
<GrRating />

Install

npm i @feugene/granularity

Import

import { GrRating } from '@feugene/granularity/components/GrRating'

API

Props

PropTypedefaultDescription
tone"primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined"warning"The tone of the fill; overridden pointwise by the `--gr-rating-color` variable.
iconstring | Component | undefinedundefinedA symbol instead of the built-in star: a Vue component or the class of an icon from your UnoCSS build (`'i-lucide-heart'` — then your `presetIcons` is needed, see `docs/installation.md`).
disabledboolean | undefinedfalse
readonlyboolean | undefinedfalseDisplay only: without input and without focus.
invalidboolean | undefinedfalseThe visual and ARIA state of an error.
requiredboolean | undefinedfalseA mandatory field (`aria-required`).
size"xs" | "sm" | "md" | "lg" | undefinedundefined
ariaLabelstring | undefinedundefined
clearableboolean | undefinedfalseA repeated click on the current score resets it to `0`.
namestring | undefinedundefinedThe name for a native form: a hidden input with the value; `0` means "not chosen", and the input is not rendered.
maxnumber | undefined5The number of symbols on the scale.
compactboolean | undefinedfalseA compact look: only the filled symbols are drawn. It makes sense together with `readonly` — in lists and tables five stars in every row eat the width.
allowHalfboolean | undefinedfalseHalf scores: a click on the left half of a symbol gives `.5`.
showTextboolean | undefinedfalseShow a numeric label to the right of the scale.
formatText((value: number) => string) | undefinedundefinedThe format of the label. By default — the value itself. Stronger than `texts`.
textsstring[] | undefinedundefinedLabels by step: "3 out of 5, fine" instead of "3 out of 5". They go both into the visible text and into `aria-valuetext` — that is what the rating exists for. An array shorter than `max` leaves the upper steps without a label.
modelValuerequirednumberThe current score. A fractional one (`3.5`) is supported with `allowHalf`.

Slots

SlotTypeDescription
symbol{ index: number; filled: boolean; }A symbol of your own instead of the star. `filled` is the filled half of the symbol.
text{ value: number; }A label beside the score.

Events

EventTypeDescription
update:modelValue[value: number]
change[value: number]
clear[]
focus[event: FocusEvent]
blur[event: FocusEvent]
hoverChange[value: number | null]

Methods / Expose

Methods / ExposeTypeDescription
focus() => void
blur() => void

Examples 3

Basic rating

4

Score: 4 — click a star, or use arrow keys, Home / End.

Basic
<script setup lang="ts">
import { ref } from 'vue'

import { GrRating } from '@feugene/granularity'

const score = ref(4)
</script>

<template>
  <div class="grid gap-4">
    <GrRating v-model="score" show-text aria-label="Rate the delivery" />

    <p class="text-sm text-[var(--gr-muted-fg)]">
      Score: <code>{{ score }}</code> — click a star, or use arrow keys, Home / End.
    </p>
  </div>
</template>

Half stars, clearable and read-only

Your rating
3.5 / 5
  • Anna K.

    Arrived a day earlier than promised.

  • Mark T.

    Good quality, packaging could be better.

  • Elena P.

    Exactly as described.

Half
<script setup lang="ts">
import { ref } from 'vue'

import { GrRating } from '@feugene/granularity'

const myScore = ref(3.5)

const reviews = [
  { author: 'Anna K.', score: 5, text: 'Arrived a day earlier than promised.' },
  { author: 'Mark T.', score: 3.5, text: 'Good quality, packaging could be better.' },
  { author: 'Elena P.', score: 4, text: 'Exactly as described.' },
]
</script>

<template>
  <div class="grid gap-6">
    <div class="grid gap-2">
      <span class="text-sm font-medium">Your rating</span>
      <GrRating
        v-model="myScore"
        allow-half
        clearable
        show-text
        :format-text="(v) => (v ? `${v} / 5` : 'Not rated')"
        aria-label="Your rating"
      />
    </div>

    <ul class="grid gap-3">
      <li v-for="review in reviews" :key="review.author" class="grid gap-1">
        <div class="flex items-center gap-2">
          <GrRating :model-value="review.score" readonly allow-half size="sm" />
          <span class="text-sm font-medium">{{ review.author }}</span>
        </div>
        <p class="text-sm text-[var(--gr-muted-fg)]">
          {{ review.text }}
        </p>
      </li>
    </ul>
  </div>
</template>

Custom symbol, tone and size

Custom symbol and tone
Own colour via CSS variable
Labels per step
Хорошо
Compact read-only (for tables and lists)
3
4.5
Sizes and disabled

Custom
<script setup lang="ts">
import { ref } from 'vue'

import { GrRating } from '@feugene/granularity'

const likes = ref(3)
const difficulty = ref(2)
const size = ref(4)

// Подписи по делениям: диктор читает «4 из 5, хорошо», а не голое число.
const service = ref(4)
const serviceTexts = ['Ужасно', 'Плохо', 'Нормально', 'Хорошо', 'Отлично']
</script>

<template>
  <div class="grid gap-6">
    <div class="grid gap-2">
      <span class="text-sm font-medium">Custom symbol and tone</span>
      <GrRating
        v-model="likes"
        icon="i-lucide-heart"
        tone="danger"
        aria-label="How much you liked it"
      />
    </div>

    <div class="grid gap-2">
      <span class="text-sm font-medium">Own colour via CSS variable</span>
      <GrRating
        v-model="difficulty"
        :max="4"
        style="--gr-rating-color: var(--gr-info)"
        aria-label="Difficulty"
      />
    </div>

    <div class="grid gap-2">
      <span class="text-sm font-medium">Labels per step</span>
      <GrRating
        v-model="service"
        :texts="serviceTexts"
        show-text
        aria-label="Service quality"
      />
    </div>

    <div class="grid gap-2">
      <span class="text-sm font-medium">Compact read-only (for tables and lists)</span>
      <div class="flex items-center gap-6">
        <GrRating :model-value="3" readonly compact show-text aria-label="Compact rating" />
        <GrRating :model-value="4.5" readonly compact allow-half show-text aria-label="Compact half rating" />
      </div>
    </div>

    <div class="grid gap-2">
      <span class="text-sm font-medium">Sizes and disabled</span>
      <div class="flex items-center gap-6">
        <GrRating v-model="size" size="sm" aria-label="Small" />
        <GrRating v-model="size" size="md" aria-label="Medium" />
        <GrRating v-model="size" size="lg" aria-label="Large" />
        <GrRating :model-value="2" disabled aria-label="Disabled" />
      </div>
    </div>
  </div>
</template>

Accessibility

APG pattern
slider (интерактивный) / img (readonly)

Full keyboard contract of the package

Component documentationAll components