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.
"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.
npx shadcn@latest add https://www.gassierconsulting.com/r/boton-magneticoEjemplo 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.
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".
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.