Account

Buttons

Free

Magnetic Button

A primary action with presence. The label travels further than the button for depth; clicks are never delayed, and it renders as a link with `href`.

Install

No key needed.

Terminal
npx @galactrex/ui add magnetic-button

Usage

Example
import { MagneticButton } from "@/components/galactrex/magnetic-button";

<MagneticButton onClick={save}>Save changes</MagneticButton>

Source

Installs to components/galactrex/magnetic-button.tsx, with the shared motion primitives beside it.

components/galactrex/magnetic-button.tsx
"use client";
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
import { motion, useTransform } from "motion/react";
import { cx } from "@/components/galactrex/motion/cx";
import { useFinePointer } from "@/components/galactrex/motion/use-fine-pointer";
import { useMagnetic } from "@/components/galactrex/motion/use-magnetic";
import { usePointer } from "@/components/galactrex/motion/use-pointer";
import { useReducedMotion } from "@/components/galactrex/motion/use-reduced-motion";

type Native = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onAnimationStart" | "onDrag" | "onDragStart" | "onDragEnd" | "style">;

export type MagneticButtonProps = Native & {
  children: ReactNode;
  /** largest pull toward the pointer in px */
  strength?: number;
  /** solid (accent fill) or outline */
  variant?: "solid" | "outline";
  /** render as a link with this address instead of a button */
  href?: string;
};

/* Fill, padding, type and colour sit on inner spans, not on the <button>: CSS resets commonly strip those from
   buttons, and a host's reset must not undo the component. The element itself carries only shape and focus. */
const BASE = "group/mag relative isolate inline-flex select-none items-center justify-center rounded-md no-underline outline-none focus-visible:ring-2 focus-visible:ring-gxui-ink focus-visible:ring-offset-2 focus-visible:ring-offset-gxui-bg disabled:pointer-events-none disabled:opacity-50";
const VARIANTS = {
  solid: { root: "", fill: "bg-gxui-accent transition-opacity group-hover/mag:opacity-90", label: "text-gxui-on-accent" },
  outline: { root: "border border-gxui-line transition-colors hover:border-gxui-ink", fill: "", label: "text-gxui-ink" },
};

/** A button that leans toward the pointer and springs back. The label travels further than the button, which
    gives the press a sense of depth. Clicks are never delayed, and the effect is off for touch, keyboard-only
    and reduced-motion visitors, where it would only get in the way. */
export const MagneticButton = forwardRef<HTMLButtonElement | HTMLAnchorElement, MagneticButtonProps>(function MagneticButton(
  { children, strength = 10, variant = "solid", href, className, ...rest },
  ref,
) {
  const fine = useFinePointer();
  const reduced = useReducedMotion();
  const pointer = usePointer();
  const enabled = fine && !reduced;
  const { x, y } = useMagnetic(pointer, strength, enabled);
  const lx = useTransform(x, (v) => v * 0.45);
  const ly = useTransform(y, (v) => v * 0.45);
  const look = VARIANTS[variant];
  const label = (
    <>
      {look.fill && <span aria-hidden className={cx("absolute inset-0 -z-10 rounded-[inherit]", look.fill)} />}
      <motion.span className={cx("relative inline-flex items-center gap-2 px-5 py-2.5 text-sm font-medium", look.label)} style={{ x: lx, y: ly }}>{children}</motion.span>
    </>
  );
  const shared = { className: cx(BASE, look.root, className), style: { x, y }, whileTap: reduced ? undefined : { scale: 0.97 }, ...pointer.handlers };
  if (href) {
    return <motion.a ref={ref as React.Ref<HTMLAnchorElement>} href={href} {...shared} {...(rest as object)}>{label}</motion.a>;
  }
  return <motion.button ref={ref as React.Ref<HTMLButtonElement>} type="button" {...shared} {...rest}>{label}</motion.button>;
});

Props

PropTypeDefaultDescription
childrenReactNode—Label and optional icon.
strengthnumber10Largest pull toward the pointer in px.
variant"solid" | "outline""solid"Accent fill or outline.
hrefstring—Render as a link.
...button propsButtonHTMLAttributes—onClick, disabled, type and the rest.

Variants

  • Solid. Accent fill for the main action.
  • Outline. Line border for secondary actions.

Accessibility

  • A real <button> (or <a> with href) with a visible focus ring.
  • The pull is off for touch, keyboard-only and reduced-motion visitors.

Motion and reduced motion

  • Spring follow through motion values; no re-render per move.
  • A 3% press scale on tap, skipped with reduced motion.