UIPackage

Rating

Vue form
Edit on GitHub

Star (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

$ npx 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