Rating
Vue formStar (or custom icon) rating control — pick a value from 1 to N. Read-only mode for displaying review averages, with half-step support.
Also available for React ->Installation
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/rating.json$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/rating.json$ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/rating.json$ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/rating.json
Or with the named registry:
npx shadcn-vue@latest add @uipkge/rating
Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
modelValue Currently selected value | number | 0 | optional |
max Maximum rating value | number | 5 | optional |
readonly If true, prevents user interaction | boolean | false | optional |
disabled If true, disables the rating | boolean | false | optional |
density Density of the component | 'compact''default''comfortable' | 'default' | optional |
color Color of the selected stars | string | 'var(--warning)' | optional |
clearable If true, clicking the same value clears the rating | boolean | false | optional |
hover If true, stars grow on hover | boolean | false | optional |
itemAriaLabel ARIA label for each rating item | string | 'rating' | optional |
emptyIcon Icon shown for empty stars | string | 'mdi-star-outline' | optional |
fullIcon Icon shown for full stars | string | 'mdi-star' | optional |
halfIcon Icon shown for half stars | string | 'mdi-star-half-full' | optional |
float If true, allows fractional values | boolean | false | optional |
size Size of the stars | 'x-small''small''medium''large''x-large' | 'medium' | optional |
class Custom class for the component | HTMLAttributes['class'] | — | optional |
showValue Show rating count (placeholder for future) | boolean | false | optional |
variant Card variant styling | 'outlined''filled''soft' | 'outlined' | optional |
halfIncrements If true, creates a half star at 0.5 | boolean | false | optional |
tooltips If true, displays tooltips on hover | string[] | — | optional |
iconSet Icon set to use ('mdi' | 'heroicons' | 'lucide') | 'mdi''heroicons''lucide' | 'mdi' | optional |
Files (2)
-
app/components/ui/rating/Rating.vue 7.4 kB
<script setup lang="ts"> import { computed } from 'vue' import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' export interface RatingProps { /** Currently selected value */ modelValue?: number /** Maximum rating value */ max?: number /** If true, prevents user interaction */ readonly?: boolean /** If true, disables the rating */ disabled?: boolean /** Density of the component */ density?: 'compact' | 'default' | 'comfortable' /** Color of the selected stars */ color?: string /** If true, clicking the same value clears the rating */ clearable?: boolean /** If true, stars grow on hover */ hover?: boolean /** ARIA label for each rating item */ itemAriaLabel?: string /** Icon shown for empty stars */ emptyIcon?: string /** Icon shown for full stars */ fullIcon?: string /** Icon shown for half stars */ halfIcon?: string /** If true, allows fractional values */ float?: boolean /** Size of the stars */ size?: 'x-small' | 'small' | 'medium' | 'large' | 'x-large' /** Custom class for the component */ class?: HTMLAttributes['class'] /** Show rating count (placeholder for future) */ showValue?: boolean /** Card variant styling */ variant?: 'outlined' | 'filled' | 'soft' /** If true, creates a half star at 0.5 */ halfIncrements?: boolean /** If true, displays tooltips on hover */ tooltips?: string[] /** Icon set to use ('mdi' | 'heroicons' | 'lucide') */ iconSet?: 'mdi' | 'heroicons' | 'lucide' } const props = withDefaults(defineProps<RatingProps>(), { modelValue: 0, max: 5, readonly: false, disabled: false, density: 'default', color: 'var(--warning)', clearable: false, hover: false, itemAriaLabel: 'rating', emptyIcon: 'mdi-star-outline', fullIcon: 'mdi-star', halfIcon: 'mdi-star-half-full', float: false, size: 'medium', showValue: false, variant: 'outlined', halfIncrements: false, iconSet: 'mdi', }) const emit = defineEmits<{ 'update:modelValue': [value: number] }>() const densityClasses = { compact: 'rating-density-compact', default: 'rating-density-default', comfortable: 'rating-density-comfortable', } const variantClasses = { outlined: 'rating-variant-outlined', filled: 'rating-variant-filled', soft: 'rating-variant-soft', } const sizeClasses = { 'x-small': 'rating-size-xs', small: 'rating-size-sm', medium: 'rating-size-md', large: 'rating-size-lg', 'x-large': 'rating-size-xl', } const componentClasses = computed(() => [ 'rating', densityClasses[props.density], variantClasses[props.variant], sizeClasses[props.size], { 'rating-readonly': props.readonly, 'rating-disabled': props.disabled, 'rating-hover': props.hover, 'rating-clearable': props.clearable, 'rating-show-value': props.showValue, }, props.class, ]) function handleClick(value: number) { if (props.disabled || props.readonly) return if (props.clearable && value === props.modelValue) { emit('update:modelValue', 0) } else { emit('update:modelValue', value) } } function handleKeydown(event: KeyboardEvent, value: number) { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault() handleClick(value) } else if (event.key === 'ArrowRight' && value < props.max) { emit('update:modelValue', value + 1) } else if (event.key === 'ArrowLeft' && value > 1) { emit('update:modelValue', value - 1) } } function getStarClass(index: number): string[] { const value = props.modelValue const classes = ['rating-star'] if (value >= index + 1) { classes.push('rating-star-full') } else if (value >= index + 0.5 && props.halfIncrements) { classes.push('rating-star-half') } else { classes.push('rating-star-empty') } return classes } </script> <template> <div data-uipkge data-slot="rating" :class="cn(...componentClasses)" role="radiogroup" :aria-valuenow="modelValue" :aria-valuemin="0" :aria-valuemax="max" :aria-label="`Rating: ${modelValue} of ${max}`" > <button v-for="n in max" :key="n" type="button" class="focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none" :class="getStarClass(n - 1)" :disabled="disabled || readonly" :aria-label="`${itemAriaLabel} ${n} of ${max}`" :tabindex="readonly || disabled ? -1 : 0" @click="handleClick(n)" @keydown="handleKeydown($event, n)" > <!-- Full Star Icon --> <svg v-if="modelValue >= n" class="star-icon" viewBox="0 0 24 24" fill="currentColor" :style="{ color: props.color }" > <path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" /> </svg> <!-- Half Star Icon --> <svg v-else-if="modelValue >= n - 0.5 && halfIncrements" class="star-icon star-half" viewBox="0 0 24 24" :style="{ color: props.color }" > <defs> <linearGradient :id="`half-${n}`"> <stop offset="50%" stop-color="currentColor" /> <stop offset="50%" stop-color="transparent" /> </linearGradient> </defs> <path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" :fill="`url(#half-${n})`" stroke="currentColor" stroke-width="1" /> </svg> <!-- Empty Star Icon --> <svg v-else class="star-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> <path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" /> </svg> </button> <span v-if="showValue" class="rating-value">{{ modelValue }}</span> </div> </template> <style scoped> .rating { display: inline-flex; align-items: center; gap: 0.125rem; } .rating-readonly { cursor: default; } .rating-disabled { cursor: not-allowed; opacity: 0.5; } .rating-hover .rating-star:hover { transform: scale(1.15); } .rating-hover .rating-star { transition: transform 0.15s ease-in-out; } .rating-clearable .rating-star { cursor: pointer; } .rating-variant-filled { background: var(--muted); padding: 0.25rem; border-radius: 0.5rem; } .rating-variant-soft { background: var(--accent); padding: 0.25rem; border-radius: 0.5rem; } .rating-star { background: none; border: none; padding: 0.125rem; line-height: 1; display: inline-flex; align-items: center; justify-content: center; } .rating-star:focus-visible { outline: 2px solid v-bind(color); outline-offset: 2px; border-radius: 2px; } /* Density */ .rating-density-compact .rating-star { padding: 0; } .rating-density-default .rating-star { padding: 0.0625rem; } .rating-density-comfortable .rating-star { padding: 0.125rem; } /* Sizes */ .rating-size-xs .star-icon { width: 0.875rem; height: 0.875rem; } .rating-size-sm .star-icon { width: 1.125rem; height: 1.125rem; } .rating-size-md .star-icon { width: 1.375rem; height: 1.375rem; } .rating-size-lg .star-icon { width: 1.625rem; height: 1.625rem; } .rating-size-xl .star-icon { width: 2rem; height: 2rem; } .rating-star-empty { color: var(--muted-foreground); } .rating-star-full { color: v-bind(color); } .rating-star-half .star-icon { position: relative; } .rating-value { margin-left: 0.5rem; font-weight: 600; color: var(--foreground); } .rating-show-value { display: flex; align-items: center; gap: 0.25rem; } </style> -
app/components/ui/rating/index.ts 0 kB
export { default as Rating } from './Rating.vue'
Raw manifest: https://uipkge.dev/r/vue/rating.json