Ir al contenido
Evaluar mi proyecto
Componentes · MicrointeraccionesDisponible

Valoración con estrellas

Cinco estrellas con previsualización al pasar el puntero, selección accesible y borrado de la valoración.

¿Qué te pareció?

3 de 5 estrellas

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

valoracion-estrellas.tsx100 líneas
Descargar
"use client";

import { useState, type CSSProperties } from "react";

/** Valoración con estrellas. React + TypeScript, sin dependencias. Personaliza con variables --gczv-*. */
export type ValoracionEstrellasProps = {
  initial?: number;
  label?: string;
  onChange?: (value: number) => void;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gczv-root { box-sizing: border-box; min-width: 0; max-width: 100%; color: var(--gczv-fg, #202421); font: 400 15px/1.5 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.gczv-root *, .gczv-root *::before, .gczv-root *::after { box-sizing: border-box; }
.gczv-root button, .gczv-root input, .gczv-root textarea, .gczv-root select { font: inherit; }
.gczv-root button, .gczv-root a { -webkit-tap-highlight-color: transparent; }
.gczv-root :focus-visible { outline: 2px solid var(--gczv-accent, #25724b); outline-offset: 4px; }
.gczv-root ::selection { background: var(--gczv-accent, #25724b); color: #fff; }
.gczv-root button:disabled { opacity: .5; cursor: not-allowed; }
.gczv-root svg { flex-shrink: 0; }
@media (prefers-reduced-motion: reduce) { .gczv-root *, .gczv-root *::before, .gczv-root *::after { animation: none !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } }
.gczv-root { text-align:center; }
.gczv-root p { font-weight:600; font-size:20px; margin:0 0 20px; }
.gczv-stars { display:flex; justify-content:center; gap:4px; margin-bottom:15px; }
.gczv-stars button { display:grid; place-items:center; width:48px; height:48px; padding:0; border:0; border-radius:6px; background:transparent; color:#b1a58e; cursor:pointer; transition:transform .2s cubic-bezier(.16,1,.3,1); }
.gczv-stars svg { fill:transparent; transition:fill .2s; }
.gczv-stars button[data-filled="true"] { color:var(--gczv-accent,#997016); }
.gczv-stars button[data-filled="true"] svg { fill:#dec58a; }
.gczv-stars button:hover { transform:translateY(-3px); }
.gczv-root>span { display:block; font-size:12px; color:#695d46; }
.gczv-reset { display:block; margin:12px auto 0; min-height:44px; padding:8px 16px; border:0; border-radius:5px; background:transparent; text-decoration:underline; text-underline-offset:4px; color:#695d46; font-size:11px!important; cursor:pointer; }`;

export function ValoracionEstrellas({
  initial = 3,
  label = "¿Qué te pareció?",
  onChange,
  className = "",
  style,
}: ValoracionEstrellasProps) {
  const [value, setValue] = useState(Math.min(5, Math.max(0, initial)));
  const [hover, setHover] = useState(0);
  return (
    <div className={`gczv-root ${className}`} style={style}>
      <style>{CSS}</style>
      <p>{label}</p>
      <div
        className="gczv-stars"
        role="group"
        aria-label={label}
        onPointerLeave={() => setHover(0)}
      >
        {[1, 2, 3, 4, 5].map((n) => (
          <button
            type="button"
            key={n}
            aria-label={`${n} de 5 estrellas`}
            aria-pressed={value === n}
            data-filled={n <= (hover || value)}
            onPointerEnter={(e) => {
              if (e.pointerType === "mouse") setHover(n);
            }}
            onClick={() => {
              setValue(n);
              onChange?.(n);
            }}
          >
            <svg
              viewBox="0 0 24 24"
              width="32"
              height="32"
              stroke="currentColor"
              strokeWidth="1.3"
              aria-hidden="true"
            >
              <path d="m12 3 2.8 5.7 6.3.9-4.6 4.4 1.1 6.3-5.6-3-5.6 3 1.1-6.3L3 9.6l6.3-.9Z" />
            </svg>
          </button>
        ))}
      </div>
      <span role="status">
        {value ? `${value} de 5 estrellas` : "Sin valoración"}
      </span>
      <button
        className="gczv-reset"
        type="button"
        disabled={!value}
        onClick={() => {
          setValue(0);
          setHover(0);
          onChange?.(0);
        }}
      >
        Borrar valoración
      </button>
    </div>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/valoracion-estrellas

Ejemplo de uso de Valoración con estrellas

Después de copiar valoracion-estrellas.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { ValoracionEstrellas } from "./valoracion-estrellas";

export default function Ejemplo() {
  return <ValoracionEstrellas label="Ejemplo" />;
}

Propiedades de ValoracionEstrellas

ValoracionEstrellas acepta 5 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

PropiedadTipoDescripción
initial?numberOpcional.
label?stringOpcional.
onChange?(value: number) => voidOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Personaliza Valoración con estrellas con variables CSS

Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.

VariableValor por defecto
--gczv-fg#202421
--gczv-accent#25724b

Accesibilidad y rendimiento

El archivo tiene 100 líneas (4 KB) y no necesita @keyframes. Esto es lo que hace Valoración con estrellas por quien lo usa:

  • Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
  • Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
  • Declara roles ARIA para lectores de pantalla: group, status.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)