Ir al contenido
Evaluar mi proyecto
Componentes · MicrointeraccionesDisponible

Me gusta con explosión de partículas

El corazón se rellena con rebote, estalla en doce partículas de color y el contador se actualiza.

Código completo

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

like-particulas.tsx76 líneas
Descargar
"use client";

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

/** Botón de "me gusta": el corazón se rellena con un rebote, estalla en partículas de color y el contador gira a su nuevo valor. React + TypeScript, sin dependencias. */
export type LikeParticulasProps = {
  defaultLiked?: boolean;
  count?: number;
  onChange?: (liked: boolean) => void;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcmlp-root { box-sizing: border-box; display: inline-block; font: 700 17px/1 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; color: #2a2230; }
.gcmlp-root *, .gcmlp-root *::before, .gcmlp-root *::after { box-sizing: border-box; }
.gcmlp-root button { font: inherit; -webkit-tap-highlight-color: transparent; }
.gcmlp-root :focus-visible { outline: 3px solid #e5265a; outline-offset: 4px; }
@media (prefers-reduced-motion: reduce) { .gcmlp-root *, .gcmlp-root *::before, .gcmlp-root *::after { animation: none !important; transition-duration: .01ms !important; } }
.gcmlp-btn { position: relative; display: inline-flex; align-items: center; gap: 12px; min-height: 52px; padding: 0 22px 0 16px; border: 1px solid #e6dfe8; border-radius: 999px; background: #fff; color: inherit; cursor: pointer; box-shadow: 0 1px 2px rgba(40, 20, 50, .08); transition: transform .15s, border-color .2s, background .2s; }
.gcmlp-btn:active { transform: scale(.94); }
.gcmlp-btn[aria-pressed="true"] { border-color: #ffc4d2; background: #fff5f8; }
.gcmlp-heart { position: relative; width: 28px; height: 28px; }
.gcmlp-heart svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.gcmlp-fill { fill: #e5265a; stroke: #e5265a; transform: scale(0); transform-origin: 50% 55%; transition: transform .2s; }
.gcmlp-btn[aria-pressed="true"] .gcmlp-fill { transform: scale(1); animation: gcmlp-pop .55s cubic-bezier(.34, 1.56, .64, 1); }
.gcmlp-line { fill: none; stroke: #6b6270; stroke-width: 1.8; stroke-linejoin: round; transition: stroke .2s; }
.gcmlp-btn[aria-pressed="true"] .gcmlp-line { stroke: #e5265a; }
.gcmlp-p { position: absolute; left: 50%; top: 50%; width: 7px; height: 7px; margin: -3.5px; border-radius: 50%; background: var(--c); opacity: 0; animation: gcmlp-burst .65s cubic-bezier(.16, 1, .3, 1) forwards; }
.gcmlp-num { position: relative; display: inline-block; height: 1.2em; overflow: hidden; min-width: 2ch; font-variant-numeric: tabular-nums; }
.gcmlp-num span { display: block; height: 1.2em; line-height: 1.2em; transition: transform .35s cubic-bezier(.16, 1, .3, 1); }
@keyframes gcmlp-pop { 0% { transform: scale(0); } 60% { transform: scale(1.3); } 100% { transform: scale(1); } }
@keyframes gcmlp-burst { 0% { opacity: 1; transform: translate(0, 0) scale(1); } 100% { opacity: 0; transform: translate(var(--x), var(--y)) scale(.2); } }`;

const COLORS = ["#e5265a", "#ff8a3d", "#ffd23f", "#b24dff", "#ff5ca8"];

export function LikeParticulas({ defaultLiked = false, count = 128, onChange, className = "", style }: LikeParticulasProps) {
  const [liked, setLiked] = useState(defaultLiked);
  const [burst, setBurst] = useState(0);
  const [base] = useState(count);
  const total = base + (liked ? 1 : 0) - (defaultLiked ? 1 : 0);

  function toggle() {
    const next = !liked;
    setLiked(next);
    if (next) setBurst((value) => value + 1);
    onChange?.(next);
  }

  return (
    <span className={`gcmlp-root ${className}`} style={style}>
      <style>{CSS}</style>
      <button className="gcmlp-btn" type="button" aria-pressed={liked} aria-label={`Me gusta, ${total} personas`} onClick={toggle}>
        <span className="gcmlp-heart" aria-hidden="true">
          <svg viewBox="0 0 24 24">
            <path className="gcmlp-fill" d="M12 21s-7.5-4.6-9.6-9.3C.8 8 3 4.5 6.5 4.5c2 0 3.5 1 5.5 3 2-2 3.5-3 5.5-3C21 4.5 23.2 8 21.6 11.7 19.5 16.4 12 21 12 21z" />
            <path className="gcmlp-line" d="M12 21s-7.5-4.6-9.6-9.3C.8 8 3 4.5 6.5 4.5c2 0 3.5 1 5.5 3 2-2 3.5-3 5.5-3C21 4.5 23.2 8 21.6 11.7 19.5 16.4 12 21 12 21z" />
          </svg>
          {burst > 0 && liked
            ? Array.from({ length: 12 }, (_, i) => {
                const angle = (i / 12) * Math.PI * 2;
                const dist = 30 + (i % 3) * 8;
                return <i key={`${burst}-${i}`} className="gcmlp-p" style={{ "--x": `${Math.cos(angle) * dist}px`, "--y": `${Math.sin(angle) * dist}px`, "--c": COLORS[i % COLORS.length] } as CSSProperties} />;
              })
            : null}
        </span>
        <span className="gcmlp-num" aria-hidden="true">
          <span key={total} style={{ transform: "none" }}>
            {total}
          </span>
        </span>
      </button>
    </span>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

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

Ejemplo de uso de Me gusta con explosión de partículas

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

ejemplo.tsx5 líneas
import { LikeParticulas } from "./like-particulas";

export default function Ejemplo() {
  return <LikeParticulas defaultLiked />;
}

Propiedades de LikeParticulas

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

PropiedadTipoDescripción
defaultLiked?booleanOpcional.
count?numberOpcional.
onChange?(liked: boolean) => voidOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Accesibilidad y rendimiento

El archivo tiene 76 líneas (5 KB) y define 2 animaciones con @keyframes. Esto es lo que hace Me gusta con explosión de partículas 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).
  • Expone su estado a tecnologías de apoyo con atributos aria-*.
  • 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)