本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100

カーソル

カーソルの反転色の円が大きな文字の上を動く

デモ

操作:カーソルを動かす。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。

仕様

用途
カーソル
動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

暗い背景の上をグレージュの円がポインタを少し遅れて追いかけ、円が重なった所だけ、背景は明るいグレージュに、明るい文字は暗いスレートカラーに変わります。円そのものは単色で、色を入れ替えているのは mix-blend-mode の difference です。下にある色と円の色との差が表示されるので、暗い所は明るく、明るい所は暗く反転して見えます。

カーソル代わりの小さな点も、同じ合成で重ねてあります。円の外ではグレージュに見える点が、円の中では差をもう一度取ることになり、元の暗い色に戻ります。円の直径は260pxで、quickTo で0.35秒かけてポインタに追いつきます。

背景色は body に塗りたくなりますが、そうすると背景が円との合成に加わらない場合があり、円が反転せずにただのグレージュの円として表示されてしまいます。背景色は円と文字を包むセクションの要素に塗り、その要素に isolation の isolate を指定して、合成をその中に閉じることにしました。円は画面に固定して置いているので、セクションの overflow では切れません。はみ出した円が下のセクションに残らないよう、セクションには clip-path を指定しています。

見出しは反転を見せるための舞台なので、最大144pxまで大きくしました。文字が太く大きいほど、円が文字と背景の両方にまたがる部分が増え、反転がよく読み取れます。和文の字幅はほぼ文字サイズと同じなので、この大きさなら1行8文字(行末の読点を含む)までに収め、行は文の切れ目で分けています。

円の色を替えると、反転の色合いも一緒に変わります。白にすると明暗だけが入れ替わり、淡い色にすると、暗い背景はその色に、明るい文字はその補色に近い暗い色になります。

コード

HTML
<!-- 円と文字を包むセクション。背景色はこのセクションに塗り、色の合成もこの中に閉じる -->
<section class="blend" aria-labelledby="blend-title">
  <h2 class="blend__title" id="blend-title">伝えたいことを、<br>まっすぐに。</h2>
  <p class="blend__lead">ここに説明文が入ります。</p>
  <a class="blend__link" href="/studio/">スタジオについて <span aria-hidden="true">→</span></a>

  <!-- 反転させる円と、カーソルの点。飾りなので読み上げから外す -->
  <span class="blend__circle" aria-hidden="true"></span>
  <span class="blend__dot" aria-hidden="true"></span>
</section>

CSS
.blend {
  --blend-bg: #111111; /* 背景色。円の中では明るく反転する */
  --blend-ink: #eeece6; /* 文字色。円の中では暗く反転する */
  --blend-color: #d2b999; /* 円と点の色。背景との差・文字との差の色になる(白なら明暗だけが入れ替わる) */
  --blend-circle: 260px; /* 円の直径 */
  --blend-dot: 12px; /* カーソルの点の直径 */
  /* 背景色は body ではなく、このセクションに塗る(body の背景は合成の下地にならないことがあり、円が色のまま表示される) */
  background: var(--blend-bg);
  color: var(--blend-ink);
  /* 合成をこのセクションの中に閉じる */
  isolation: isolate;
  /* 画面に固定した円は overflow では切れないので、セクションの外へはみ出す分は clip-path で切る */
  clip-path: inset(0);
  padding: 144px clamp(24px, 7vw, 96px) 96px;
}

.blend__title {
  margin: 0;
  font-size: clamp(34px, 10vw, 144px);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.02em;
}

.blend__lead { margin: 48px 0 24px; font-size: 18px; line-height: 1.8; letter-spacing: 0.04em; word-break: auto-phrase; }

.blend__link {
  color: inherit;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-underline-offset: 0.4em;
}

.blend__link:focus-visible { outline: 2px solid currentColor; outline-offset: 4px; }

.blend__circle,
.blend__dot {
  position: fixed;
  left: 0;
  top: 0;
  border-radius: 50%;
  /* 円の色との差を取るので、暗い背景は円の色に近く、明るい文字は補色寄りの暗い色になる */
  background: var(--blend-color);
  mix-blend-mode: difference;
  /* 下のリンクを押せるようにする */
  pointer-events: none;
  /* JS が有効にしたときだけ表示する。タッチ端末やスクリプトが動かないときは、本物のカーソルのまま */
  display: none;
}

.blend__circle { width: var(--blend-circle); height: var(--blend-circle); }
.blend__dot { width: var(--blend-dot); height: var(--blend-dot); }

.blend.has-blend-cursor .blend__circle,
.blend.has-blend-cursor .blend__dot {
  display: block;
}

/* 本物のカーソルは、セクションの中で円を表示したときだけ隠す(点がカーソルの代わりになる) */
.blend.has-blend-cursor,
.blend.has-blend-cursor a {
  cursor: none;
}

JavaScript
import { gsap } from "gsap";

// タッチ端末にはポインタが無いので、円は表示せず本物のカーソルのままにする
const canHover = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const CHASE = 0.35; // 円がポインタに追いつく時間(秒)
const GROW = reduceMotion ? 0 : 0.3; // セクションに出入りしたとき、円が現れる・消える時間(秒)

document.querySelectorAll(".blend").forEach((section) => {
  const circle = section.querySelector(".blend__circle");
  const dot = section.querySelector(".blend__dot");
  if (!canHover || !circle || !dot) return;

  // 中心をポインタに合わせる。セクションに入るまでは大きさ 0 で隠しておく
  gsap.set([circle, dot], { xPercent: -50, yPercent: -50, scale: 0 });
  section.classList.add("has-blend-cursor");
  // 動きを減らす設定では遅れなし(quickTo は duration が 0 だと値が動かない)
  const follow = (prop) => (reduceMotion ? (value) => gsap.set(circle, { [prop]: value }) : gsap.quickTo(circle, prop, { duration: CHASE, ease: "power3" }));
  const moveX = follow("x");
  const moveY = follow("y");
  const place = (x, y) => {
    gsap.set(dot, { x, y }); // 点は遅れなし
    moveX(x);
    moveY(y);
  };
  const show = (on) => {
    gsap.to([circle, dot], { scale: on ? 1 : 0, duration: GROW, ease: on ? "power3.out" : "power2.in", overwrite: "auto" });
  };

  section.addEventListener("pointerenter", (event) => {
    // 入った位置から表示する。前に出ていった位置から飛んでこないよう、円も先に置いておく
    gsap.set(circle, { x: event.clientX, y: event.clientY });
    place(event.clientX, event.clientY);
    show(true);
  });
  section.addEventListener("pointermove", (event) => place(event.clientX, event.clientY));
  section.addEventListener("pointerleave", () => show(false));

  // キーボードで中のリンクへ移ったときは、円をそのリンクの上に置いて反転させる(マウスで押したときの focus は除く)
  section.addEventListener("focusin", (event) => {
    if (!event.target.matches(":focus-visible")) return;
    const box = event.target.getBoundingClientRect();
    place(box.left + box.width / 2, box.top + box.height / 2);
    show(true);
  });
  section.addEventListener("focusout", () => {
    if (!section.matches(":hover")) show(false);
  });
});

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。