Ir al contenido
Evaluar mi proyecto
Componentes · BotonesDisponible

Botón magnético

Sigue al puntero, el texto rueda y un color sube desde abajo.

Código completo

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

magnetic-button.tsx78 líneas
Descargar
"use client";

/**
 * Botón magnético: se inclina hacia el puntero, el texto rueda y un color sube desde abajo.
 * Con `prefers-reduced-motion` solo cambia el color.
 *
 *   <MagneticButton>Hablemos</MagneticButton>
 *   <MagneticButton accent="#ff5a1f">Ver más</MagneticButton>
 */

import { useRef, type ButtonHTMLAttributes, type CSSProperties, type PointerEvent } from "react";

export type MagneticButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> & {
  /** Texto del botón (se repite para el efecto de rodar). */
  children: string;
  /** Color que sube al pasar el puntero. */
  accent?: string;
  /** Qué tanto sigue al puntero, de 0 a 1. */
  strength?: number;
};

const CSS = `
.gcm-wrap { display: inline-block; padding: 18px; margin: -18px; }
.gcm-btn { position: relative; display: inline-flex; overflow: hidden; align-items: center; justify-content: center; min-height: 58px; padding: 0 38px; border: 0; border-radius: 999px;
  background: #111; color: #fff; font-family: inherit; font-size: 16px; font-weight: 600; line-height: 1; cursor: pointer; transition: transform .6s cubic-bezier(.34, 1.56, .64, 1); will-change: transform; }
.gcm-btn[data-active="true"] { transition: transform .12s linear; }
.gcm-btn::before { content: ""; position: absolute; inset: 0; background: var(--gcm-accent); border-radius: 50% 50% 0 0 / 100% 100% 0 0; transform: translateY(101%);
  transition: transform .5s cubic-bezier(.16, 1, .3, 1), border-radius .5s cubic-bezier(.16, 1, .3, 1); }
.gcm-btn:hover::before, .gcm-btn:focus-visible::before { transform: translateY(0); border-radius: 0; }
.gcm-label { position: relative; display: block; height: 1em; overflow: hidden; }
.gcm-label > span { display: block; height: 1em; transition: transform .5s cubic-bezier(.16, 1, .3, 1); }
.gcm-label > span + span { color: #111; }
.gcm-btn:hover .gcm-label > span, .gcm-btn:focus-visible .gcm-label > span { transform: translateY(-100%); }
.gcm-btn:focus-visible { outline: 2px solid #111; outline-offset: 5px; }
.gcm-btn:disabled { opacity: .5; cursor: not-allowed; }
@media (prefers-reduced-motion: reduce) {
  .gcm-btn, .gcm-btn::before, .gcm-label > span { transition: none; }
  .gcm-label > span { transform: none !important; }
  .gcm-label > span + span { display: none; }
  .gcm-btn:hover { background: var(--gcm-accent); color: #111; }
}
`;

export function MagneticButton({ children, accent = "#c6ff3a", strength = 0.35, className, style, ...props }: MagneticButtonProps) {
  const ref = useRef<HTMLButtonElement>(null);

  function move(event: PointerEvent<HTMLSpanElement>) {
    const button = ref.current;
    if (!button || event.pointerType === "touch" || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    const box = button.getBoundingClientRect();
    const dx = event.clientX - (box.left + box.width / 2);
    const dy = event.clientY - (box.top + box.height / 2);
    button.dataset.active = "true";
    button.style.transform = `translate(${(dx * strength).toFixed(1)}px, ${(dy * strength).toFixed(1)}px)`;
  }

  function leave() {
    const button = ref.current;
    if (!button) return;
    button.dataset.active = "false";
    button.style.transform = "";
  }

  return (
    <>
      <style>{CSS}</style>
      <span className="gcm-wrap" onPointerMove={move} onPointerLeave={leave}>
        <button ref={ref} type="button" {...props} className={["gcm-btn", className].filter(Boolean).join(" ")} style={{ ["--gcm-accent" as string]: accent, ...style } as CSSProperties}>
          <span className="gcm-label">
            <span>{children}</span>
            <span aria-hidden="true">{children}</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/boton-magnetico

Ejemplo de uso de Botón magnético

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

ejemplo.tsx5 líneas
import { MagneticButton } from "./magnetic-button";

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

Accesibilidad y rendimiento

El archivo tiene 78 líneas (4 KB) y no necesita @keyframes. Esto es lo que hace Botón magnético 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-*.
  • Reacciona al puntero con eventos pointer (ratón, lápiz y táctil).
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React · Next.js (quita la directiva use client fuera de Next.js) · Sin Tailwind

Acerca del producto

Una píldora que se inclina hacia el puntero con un rebote al soltarlo, mientras el texto rueda hacia arriba y un color sube desde abajo. Con movimiento reducido solo cambia de color.

Incluye

Un solo archivo React + TypeScript, con los estilos dentro. No usa Tailwind ni bibliotecas externas.

  • Atracción magnética configurable (strength)
  • Texto que rueda sin duplicarlo para lectores de pantalla
  • Color de relleno configurable (accent)
  • Ignora pantallas táctiles

Cómo usarlo

Pasa el texto como hijo (string). El área magnética es un poco mayor que el botón.