vue-star-rate
Zero-dependency, WCAG 2.2 accessible star rating for Vue 3.5+.
⭐ vue-js-star-rating
Zero-dependency Vue 3.5+ star rating — WCAG 2.2 accessible, TypeScript-first, half-star precision, Lucide / FontAwesome / custom icons
✨ Features
| Feature | Description |
|---|---|
| 🎯 Half-star ratings | Precise 0.5-step ratings with visual half-fill support |
| 📐 Size presets | Five built-in sizes — xs · sm · md · lg · xl |
| 🎨 Custom colors | Per-state color tokens: empty, filled, hover, half |
| 🔌 Icon providers | Lucide, FontAwesome, or bring your own via scoped slot/render fn |
| ⌨️ Keyboard navigation | Arrow keys, Home, End, 0–9 — fully operable without mouse |
| 🔒 Read-only mode | Display-only ratings for reviews and dashboards |
| ♿ WCAG 2.2 accessible | role="group", aria-pressed per star, aria-live counter |
| 🔷 TypeScript-first | Full generics, MaybeRefOrGetter<T> reactive options |
| ⚡ Zero dependencies | No runtime deps — only Vue 3.5+ as a peer |
| 🌐 RTL support | Right-to-left layout via :rtl="true" |
| 🔁 Clearable / reset | Built-in clear button and allow-reset-on-reclick behaviour |
| 🧩 Slots API | Override icon, counter, and clear button with your own markup |
📦 Installation
# pnpm (recommended)
pnpm add vue-js-star-rating
# npm
npm install vue-js-star-rating
# yarn
yarn add vue-js-star-rating
Requires Vue 3.5+. This package uses
defineModel,useTemplateRef, andMaybeRefOrGetter— all stable in Vue 3.5. It has zero runtime dependencies.
🚀 Quick Start
<script setup lang="ts">
import { ref } from 'vue';
import { VueStarRate } from 'vue-js-star-rating';
import 'vue-js-star-rating/dist/style.css';
const rating = ref(0);
</script>
<template>
<VueStarRate v-model="rating" />
</template>
Global Plugin Registration
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import VueStarRatePlugin from 'vue-js-star-rating';
import 'vue-js-star-rating/dist/style.css';
createApp(App).use(VueStarRatePlugin).mount('#app');
📖 Examples
Half-Star Ratings
<VueStarRate v-model="rating" :allow-half="true" :show-counter="true" />
Size Presets
<!-- Size Presets: xs(16px), sm(20px), md(24px), lg(32px), xl(40px) -->
<VueStarRate v-model="rating" size="xs" />
<VueStarRate v-model="rating" size="sm" />
<VueStarRate v-model="rating" size="md" />
<VueStarRate v-model="rating" size="lg" />
<VueStarRate v-model="rating" size="xl" />
<!-- Custom pixel size -->
<VueStarRate v-model="rating" :icon-size="28" />
Custom Colors
<VueStarRate
v-model="rating"
:colors="{
empty: '#e5e7eb',
filled: '#fbbf24',
hover: '#fcd34d',
half: '#fbbf24',
}"
/>
Read-only Display
<VueStarRate v-model="rating" :readonly="true" :show-counter="true" />
Clearable Rating
<VueStarRate v-model="rating" :clearable="true" />
RTL Layout
<VueStarRate v-model="rating" :rtl="true" />
Icon Providers
<!-- Lucide icons (requires lucide-vue-next) -->
<VueStarRate v-model="rating" icon-provider="lucide" />
<!-- FontAwesome (requires @fortawesome/fontawesome-free CSS) -->
<VueStarRate v-model="rating" icon-provider="fontawesome" />
<!-- Custom slot icon -->
<VueStarRate v-model="rating" icon-provider="custom">
<template #icon="{ filled, size, color }">
<svg :width="size.width" :height="size.height" viewBox="0 0 24 24">
<polygon points="12,2 15,9 22,9 17,14 19,21 12,17 5,21 7,14 2,9 9,9"
:fill="filled ? color : 'none'" :stroke="color" stroke-width="2" />
</svg>
</template>
</VueStarRate>
Custom Star Count
<VueStarRate v-model="rating" :max-stars="10" />
Counter & Tooltips
<!-- Show counter -->
<VueStarRate v-model="rating" :show-counter="true" />
<!-- Custom counter template -->
<VueStarRate v-model="rating" :show-counter="true" counter-template="{value} / {max} stars" />
<!-- Tooltips on hover -->
<VueStarRate v-model="rating" :show-tooltip="true" />
<!-- Custom tooltip labels -->
<VueStarRate
v-model="rating"
:show-tooltip="true"
:tooltip-labels="['Terrible', 'Bad', 'OK', 'Good', 'Excellent']"
/>
Full Configuration
<VueStarRate
v-model="rating"
:max-stars="5"
:allow-half="true"
:readonly="false"
:show-counter="true"
:show-tooltip="true"
size="lg"
:colors="{
empty: '#27272a',
filled: '#fbbf24',
hover: '#fcd34d',
half: '#fbbf24',
}"
:animation="{
enabled: true,
duration: 200,
type: 'scale',
}"
@change="(val, old) => console.log(val, old)"
@hover="(val) => console.log('hover:', val)"
/>
📋 Props API
| Prop | Type | Default | Description |
|---|---|---|---|
v-model |
number |
0 |
Rating value |
maxStars |
number |
5 |
Maximum number of stars |
allowHalf |
boolean |
false |
Enable half-star ratings |
size |
'xs' | 'sm' | 'md' | 'lg' | 'xl' |
'md' |
Size preset |
iconSize |
number | { width, height } |
auto |
Custom pixel size |
iconProvider |
'custom' | 'lucide' | 'fontawesome' |
'custom' |
Icon renderer |
colors |
Partial<StarColors> |
see below | Color configuration |
animation |
Partial<AnimationConfig> |
see below | Animation configuration |
readonly |
boolean |
false |
Disable user interaction |
disabled |
boolean |
false |
Disable + grey out component |
clearable |
boolean |
false |
Show a clear button when rating > 0 |
allowReset |
boolean |
true |
Click active star again to reset |
showCounter |
boolean |
false |
Show rating counter |
counterTemplate |
string |
'{value} / {max}' |
Counter format template |
showTooltip |
boolean |
false |
Show tooltips on star hover |
tooltipLabels |
string[] |
— | Custom tooltip labels per star |
minRating |
number |
0 |
Minimum selectable value |
step |
number |
1 |
Increment step (overridden by allowHalf) |
rtl |
boolean |
false |
Right-to-left layout |
inline |
boolean |
false |
Inline layout mode |
gap |
number |
4 |
Gap between stars in pixels |
ariaLabel |
string |
'Star rating' |
Accessible group label |
className |
string |
— | Extra CSS class on root element |
faIcons |
{ empty?, filled?, half? } |
— | FontAwesome class overrides |
customIcon |
CustomIconRenderer |
— | Programmatic icon render function |
StarColors type
interface StarColors {
empty: string // default '#d1d5db' — unfilled star
filled: string // default '#fbbf24' — filled star
hover: string // default '#f59e0b' — hovered star
half?: string // default = filled — half-filled star
}
AnimationConfig type
interface AnimationConfig {
enabled: boolean // default true
duration: number // default 200 ms
type: 'scale' | 'bounce' | 'pulse' | 'none' // default 'scale'
}
🎰 Slots
| Slot | Scoped props | Description |
|---|---|---|
icon |
{ filled, half, size, color, index } |
Custom icon per star |
counter |
{ value, max } |
Custom counter content |
clear |
— | Custom clear button content |
📡 Events
| Event | Payload | Description |
|---|---|---|
change |
(value, oldValue) |
Rating changed by user interaction |
hover |
number | null |
Hover enters / leaves a star |
focus |
— | Component received keyboard focus |
blur |
— | Component lost focus |
update:modelValueis handled automatically byv-model/defineModel.
⌨️ Keyboard Navigation
| Key | Action |
|---|---|
→ ↑ |
Increase rating |
← ↓ |
Decrease rating |
Home |
Set to minimum |
End |
Set to maximum |
1-9 |
Jump to specific value |
0 |
Reset to minimum |
🖥️ Exposed Methods
const ratingRef = ref<InstanceType<typeof VueStarRate>>();
ratingRef.value?.reset(); // Reset to initial value
ratingRef.value?.setRating(3.5); // Set programmatically
ratingRef.value?.getRating(); // Get current value
ratingRef.value?.focus(); // Focus the component
ratingRef.value?.blur(); // Blur the component
🔄 Migrating from v2 to v3
Breaking Changes
| v2.x | v3.x |
|---|---|
modelValue prop |
Managed by defineModel — template syntax unchanged |
lucideIcons prop |
Removed — use icon-provider="lucide" instead |
role="slider" on container |
role="group" — WCAG 2.2 compliant |
animation: { scale: 1.15 } |
animation: { type: 'scale' } — no numeric scale key |
peerDependencies: vue ^3.3.0 |
peerDependencies: vue ^3.5.0 |
Options API data()/methods |
Composition API + <script setup> / defineModel |
Animation Migration
<!-- v2 — scale was never a valid AnimationConfig key -->
:animation="{ enabled: true, duration: 200, scale: 1.15 }"
<!-- v3 — use type instead -->
:animation="{ enabled: true, duration: 200, type: 'scale' }"
📦 Bundle size & optional peers
vue-js-star-rating is zero-dependency at the package boundary. The default SVG icon ships inside the main bundle; Lucide and FontAwesome icon providers are loaded lazily via defineAsyncComponent, so their optional peers are only required — and only fetched at runtime — when you actually render them.
| What you import | What's pulled into your bundle |
|---|---|
import { VueStarRate } from 'vue-js-star-rating' + default icon |
Component + SVG star only (~3 KB gzipped) |
<VueStarRate icon-provider="lucide" /> |
Above + a separate LucideIcon-[hash].js chunk + lucide-vue-next |
<VueStarRate icon-provider="fontawesome" /> |
Above + a separate FontAwesomeIcon-[hash].js chunk |
<VueStarRate icon-provider="custom"><template #icon /></VueStarRate> |
Component only — you supply the markup |
If you choose a non-Lucide provider, you do not need to install lucide-vue-next — the consumer bundler will not try to resolve it at build time. (Issue #7 regression test ships in tests/lazyIcons.spec.ts.)
# Only when you set icon-provider="lucide"
pnpm add lucide-vue-next
# Only when you set icon-provider="fontawesome"
# (the CSS classes the wrapper emits ship with the free pack)
pnpm add @fortawesome/fontawesome-free
❓ FAQ
Do I need lucide-vue-next if I use FontAwesome (or my own icon)?
No. Since v3.1.0, icon providers are loaded with defineAsyncComponent. The static dependency graph from import { VueStarRate } from 'vue-js-star-rating' does not include lucide-vue-next, so consumer bundlers (Vite, Webpack, Rollup, esbuild) will not require it to be installed.
Is it SSR-safe? Does it work with Nuxt 3?
Yes. The component renders deterministically on the server. Async icon providers resolve on the client after hydration — wrap your usage in <ClientOnly> (Nuxt) or <Suspense> if you need the icon present in the initial HTML, otherwise the default SVG provider works fully server-side.
What is the bundle size?
About 3 KB gzipped for the default SVG path (component + composable + types stripped). Lucide and FontAwesome chunks are emitted as separate files and only fetched when the matching provider is rendered.
Is it accessible?
The root carries role="group" + aria-label. Each star button exposes aria-pressed. The counter uses aria-live="polite" for live announcements. Full keyboard navigation: arrow keys, Home, End, 0–9. Animations are gated on prefers-reduced-motion since v3.1.0. Compliant with WCAG 2.2 Level AA.
Can I use it without v-model?
Yes — pass :model-value and listen to @change (or @update:modelValue). The composable useStarRating is also exported if you want headless control.
How do I clear the rating programmatically?
const star = ref<InstanceType<typeof VueStarRate>>();
star.value?.setRating(0); // or star.value?.reset()
⚖️ Comparison
| Feature | vue-js-star-rating | vue-star-rating |
vue3-star-ratings |
|---|---|---|---|
Vue 3.5+ (defineModel) |
✅ | — | partial |
| TypeScript-first | ✅ | — | partial |
| Runtime dependencies | 0 | 1 | 1 |
| Tree-shakeable lazy icons | ✅ | — | — |
| Half-star precision | ✅ | ✅ | ✅ |
| Keyboard navigation | ✅ (full) | partial | partial |
aria-pressed + aria-live |
✅ | — | — |
| WCAG 2.2 compliant | ✅ | — | — |
| RTL | ✅ | — | — |
prefers-reduced-motion |
✅ | — | — |
| Custom icon via slot | ✅ | partial | — |
| SSR / Nuxt 3 | ✅ | partial | ✅ |
🛠️ Development
# Clone the repository
git clone https://github.com/pooyagolchian/vue-star-rate.git
cd vue-star-rate
# Install dependencies
pnpm install
# Start development server
pnpm dev
# Run tests
pnpm test
# Build for production
pnpm build
# Documentation development
pnpm docs:dev
# Build documentation
pnpm docs:build
🤝 Contributing
Contributions are welcome! Please read our Contributing Guide for details on our code of conduct and the process for submitting pull requests.
📄 License
Built with ❤️ for the Vue.js community