【Next.js】マグネティックボタンの作り方をカーソルに吸い付く動きまで解説

公開日: 2026年10月8日 · 読了目安 8 分

Next.js でカーソルに引き寄せられるマグネティックボタンを作る手順です。ずれを計算する式、Motion の useMotionValue と useSpring による滑らかな動き、強さの上限、タッチ端末と動きを減らす設定での停止、押せる範囲の保ち方までコード付きで解説します。

マグネティックボタンはカーソルに少しだけ寄せて作る

Next.js でポートフォリオやサービスの LP を作っていて、ヒーローのボタンをもう少し印象的にしたくなった。

海外のサイトで見かける、カーソルを近づけるとボタンが吸い寄せられる動きです。これはマグネティックボタンと呼ばれます。

作り方の答えは次のとおりです。カーソルとボタンの中心の差に小さな係数を掛け、その分だけボタンをずらします。

ずらす値は Motion(旧 Framer Motion)の useMotionValue に入れ、useSpring でばねのように追わせます。カーソルが離れたときも、ばねの動きで元の位置へ戻ります。

この記事で分かることは次のとおりです。

  • ボタンをずらす量を、ポインターの位置から計算する式

  • useMotionValue と useSpring で、滑らかに追って戻す方法

  • 動きの強さに上限を付ける理由と目安の数字

  • タッチ端末と動きを減らす設定で止める方法と、押せる範囲の保ち方

マグネティックボタンを使う理由と使いどころ

この動きの役目は、ページで一番押してほしいボタンに視線を集めることです。理由は2つあります。

1つ目は、カーソルに反応して動くことで「押せる」ことが伝わるからです。ボタンの少し手前から反応するので、色が変わるだけのホバーより早く気づけます。

2つ目は、静かなページに1か所だけ動きを置くと、そこが目立つからです。

そのため、使うのは1ページに1つの主な CTA(申し込みなど、押してほしいボタン)だけにします。ヒーローのボタンか、ページ最後の申し込みボタンが向いています。

ナビゲーションのリンクや、カードが並ぶ一覧のボタンには付けません。どれもが動くと、どれが大事か分からなくなります。

Step1:マグネティックボタンのずれをポインターの位置から計算する

まず、ボタンをどれだけずらすかを計算する関数を書きます。画面の部品に依存しない計算なので、普通の TypeScript のファイルに置けます。

考え方は、ポインターとボタンの中心の差に係数を掛けるだけです。係数 0.3 なら、中心から 20px 右にカーソルがあるとき、ボタンは 6px 右に寄ります。

lib/magnetic.ts
type Box = { left: number; top: number; width: number; height: number };

// strength: 寄る強さ(0〜1) / max: ずれの上限(px)
export function getMagneticOffset(
  pointer: { x: number; y: number },
  box: Box,
  strength = 0.3,
  max = 12,
) {
  const centerX = box.left + box.width / 2;
  const centerY = box.top + box.height / 2;
  const clamp = (value: number) => Math.max(-max, Math.min(max, value));

  return {
    x: clamp((pointer.x - centerX) * strength),
    y: clamp((pointer.y - centerY) * strength),
  };
}

ずれには必ず上限を付ける

係数を掛けるだけだと、大きなボタンほど遠くまで動きます。幅 240px のボタンの右端は中心から 120px なので、係数 0.3 で 36px もずれます。

これでは文字が読みにくく、隣の要素にも重なります。上限は 8〜12px ほどが目安で、寄っていることは伝わりつつボタンの形は崩れません。

測る基準は動かない外枠にする

位置を測る要素がボタン自身だと、ボタンがずれるたびに中心も動きます。ずれた位置を基準に計算し直すので、動きが小刻みに震えることがあります。

そこで、動かない外枠を測り、中のボタンだけを動かします。次の Step2 はこの形で作っています。

Step2:useMotionValue と useSpring でマグネティックボタンを動かす

次に、ボタンの部品を作ります。ポインターの動きを受け取るので、ファイルの先頭にブラウザで動かす印("use client")を書きます。

useMotionValue は、画面を描き直さずに値を更新できる Motion の入れ物です。useSpring はその値をばねの動きで追いかけ、0 に戻したときも滑らかに帰ります。

components/MagneticButton.tsx
"use client";

import Link from "next/link";
import { useRef, type PointerEvent, type ReactNode } from "react";
import { motion, useMotionValue, useReducedMotion, useSpring } from "motion/react";
import { getMagneticOffset } from "@/lib/magnetic";

const spring = { stiffness: 250, damping: 18, mass: 0.5 };

type Props = { href: string; children: ReactNode };

export function MagneticButton({ href, children }: Props) {
  const areaRef = useRef<HTMLDivElement>(null);
  const x = useMotionValue(0);
  const y = useMotionValue(0);
  const springX = useSpring(x, spring);
  const springY = useSpring(y, spring);

  const reduceMotion = useReducedMotion();
  const enabled = !reduceMotion; // Step3 でタッチ端末の判定を足す

  function handlePointerMove(event: PointerEvent<HTMLDivElement>) {
    if (!enabled || event.pointerType !== "mouse" || !areaRef.current) return;
    // 測るのは動かない外枠。ボタン自身を測ると動きが震える
    const box = areaRef.current.getBoundingClientRect();
    const offset = getMagneticOffset({ x: event.clientX, y: event.clientY }, box);
    x.set(offset.x);
    y.set(offset.y);
  }

  function handlePointerLeave() {
    x.set(0);
    y.set(0);
  }

  return (
    <div
      ref={areaRef}
      onPointerMove={handlePointerMove}
      onPointerLeave={handlePointerLeave}
      className="-m-4 inline-block p-4"
    >
      <motion.div style={{ x: springX, y: springY }} className="inline-block">
        <Link
          href={href}
          className="inline-flex min-h-11 items-center rounded-full bg-zinc-900 px-7 text-sm font-medium text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-zinc-900"
        >
          {children}
        </Link>
      </motion.div>
    </div>
  );
}

コードのポイント

  • 外枠に p-4 と -m-4 を付け、ボタンの周り 16px から反応させる。周りのレイアウトはずれない

  • ポインターが動いても値を書き換えるだけなので、部品は描き直されない

  • pointerType が mouse のときだけ動かし、指のタッチでは反応させない

  • 外枠から出たら 0 に戻し、ばねの動きで元の位置へ帰す

ページでは <MagneticButton href="/signup">無料で始める</MagneticButton> のように使います。

ばねの数値の目安

stiffness は引っ張る強さ、damping は揺れを抑える強さです。stiffness を上げると速く追い、damping を下げると戻るときに少し揺れます。

ボタンでは、揺れすぎない値が扱いやすいです。コードの値から試して、好みに合わせて少しずつ変えてください。

ページ内のリンクは Link のまま使う

動かすのは外側の motion.div で、中身は next/link の Link のままです。リンク先の先読み(プリフェッチ)など、Next.js のリンクの機能はそのまま使えます。

Step3:タッチ端末と動きを減らす設定ではマグネティックボタンを止める

スマホやタブレットには、カーソルがありません。タップした位置でボタンがずれると、押した瞬間に形が動いて見えます。

そこで、メディアクエリの (hover: hover) and (pointer: fine) を使います。マウスのように細かく指せる入力がないときは、動きを止めます。

OS で動きを減らす設定にしている人には、Motion の useReducedMotion が true を返します。この場合も止めます。

hooks/useFinePointer.ts
"use client";

import { useSyncExternalStore } from "react";

const QUERY = "(hover: hover) and (pointer: fine)";

function subscribe(onChange: () => void) {
  const media = window.matchMedia(QUERY);
  media.addEventListener("change", onChange);
  return () => media.removeEventListener("change", onChange);
}

export function useFinePointer() {
  return useSyncExternalStore(
    subscribe,
    () => window.matchMedia(QUERY).matches,
    () => false, // サーバーでは「マウスなし」として扱う
  );
}

Step2 の部品に判定を足す

Step2 の部品で、const finePointer = useFinePointer(); を足します。

そのうえで、enabled の行を const enabled = finePointer && !reduceMotion; に変えます。

サーバーで描くときは false を返すので、最初の表示はどの端末でも止まった状態です。読み込みが終わってから、マウスで操作する端末だけで動き出します。

マグネティックボタンの押せる範囲とアクセシビリティ

動くボタンでは、見た目が動いても押そうとした位置から外れないことが大切です。

この記事の形では、ボタン自体が動くので、押せる範囲も見た目と一緒に動きます。ずれの上限は 12px なので、外枠の余白 16px の中に収まります。

そのほかに、次の点を確かめます。

  • ボタンの高さは 44px 以上にし、スマホでも押しやすくする

  • キーボードで移動したときの枠線(focus-visible)を消さない

  • 動かすのは位置だけにし、文字の大きさや色まで変えない

  • 文言は、動きがなくても意味が伝わる言葉にする

マグネティックボタンでよくある失敗と公開前チェック

  • ページ中のボタン全部に付ける。動かすのは主な CTA の1つだけにする

  • 上限を付けずに大きく動かす。文字が読めず、隣の要素に重なる

  • ボタン自身の位置を測る。動きが震えるので、動かない外枠を測る

  • スマホでも動かす。タップのたびにボタンがずれて見える

  • 動きを減らす設定を無視する。設定している人には止まったボタンを見せる

  • page.tsx 全体に "use client" を書く。ボタンの部品だけに切り出す

公開前に、マウス・スマホ・キーボードの3つで押してみてください。カーソルの見た目ごと作り替える方法は https://design-layer.com/ja/articles/nextjs-custom-cursor-guide で解説しています。

完成したマグネティックボタンをコピーして使う方法

DesignLayer の「マグネティック CTA ボタン」の詳細ページ。左でライブプレビューを確かめ、右の「Cursor 用にコピー」でソースをコピーできる
DesignLayer の「マグネティック CTA ボタン」の詳細ページ。左でライブプレビューを確かめ、右の「Cursor 用にコピー」でソースをコピーできる

ばねの数値や反応する範囲を、ボタンの大きさに合わせて詰めるのは手間がかかります。

その場合は、完成した部品をブラウザで動かして確かめてから、自分の Next.js に載せる方法もあります。

DesignLayer の「マグネティック CTA ボタン」は、カーソルに引き寄せられる CTA ボタンです。詳細ページのライブプレビューで、寄り方を確かめられます。

「Cursor 用にコピー」を押すと、組み込みの指示・ライブプレビューの URL・本体の TSX・npm の依存がまとめてコピーされます。Cursor や Claude に貼れば、自分のプロジェクトに組み込めます。

閲覧とライブプレビューは無料で、アカウントもカードも要りません。無料のソースコピーは日本時間で1日3回までです。

カタログは https://design-layer.com/ja/components から開けます。Plus(月 990 円)と Pro(月 2,980 円)では、コンポーネントのコピーが無制限です。

マグネティックボタンについてよくある質問

  • 作れます。ポインターの動きで transform を書き換え、CSS の transition で戻す形です。

    ただし動いている途中で向きが変わると、動きが途切れて見えることがあります。ばねのように滑らかに追わせたいなら、Motion の useSpring が手軽です。

まとめ:Next.js でマグネティックボタンを作る手順

  • 使うのは1ページに1つ、主な CTA だけにする

  • ずれはポインターと中心の差に係数を掛け、上限を付けて計算する

  • 測るのは動かない外枠、動かすのは中のボタンにする

  • useMotionValue に値を入れ、useSpring で滑らかに追わせて戻す

  • マウス以外の端末と動きを減らす設定では止める

まずは Step1 と Step2 のコードを、ヒーローのボタン1つに付けてみてください。ヒーロー全体の動きは https://design-layer.com/ja/articles/nextjs-animated-hero-section-guide で、Motion の基本は https://design-layer.com/ja/articles/nextjs-framer-motion-guide で解説しています。

【Next.js】マグネティックボタンの作り方をカーソルに吸い付く動きまで解説 | DesignLayer