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

ヒーローの背景でドットグリッドに中心から波紋が広がる

デモ

仕様

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

しくみと調整

点を並べたグリッドに、GSAPのstaggerで中心から外へ向かう遅れを配っています。中心に近い点ほど早く動き出し、遠い点ほど遅れて動くので、輪が外へ広がるように見えます。

yoyoとrepeatは、この設定の中に書きます。外側に書くと、点全体がまとめて往復する動きになってしまいます。

常に動き続ける背景の飾りなので、動きを減らす設定のときは波紋を出さず、小さな点が並んだ静かな見た目のまま止めています。色の変化も、波が通り過ぎる一瞬だけの装飾として扱ってください。

行数・列数や点と点の間隔を変えると、グリッドの密度や余白の印象を調整できます。色を別の色に替えれば、ブランドカラーにも合わせやすくなります。背景の装飾や、読み込み中の表示に向いています。

コード

HTML
<section class="dot-hero">
  <!-- 背景は飾りなので、読み上げからは外す -->
  <div class="dot-hero__grid" aria-hidden="true"></div>
  <div class="dot-hero__inner">
    <p class="dot-hero__eyebrow">Community</p>
    <h1 class="dot-hero__title">つながりが、<br>広がっていく。</h1>
    <p class="dot-hero__lead">ここに説明文が入ります。</p>
    <a class="dot-hero__button" href="/signup/">無料ではじめる</a>
  </div>
</section>

CSS
.dot-hero {
  --bg: #f2f1ed;
  --ink: #262626;
  --ripple: #ba7069; /* 波紋が通るときの色(くすみローズ) */
  --gutter: clamp(24px, 7vw, 96px);
  position: relative;
  display: grid;
  align-items: center;
  min-height: min(90vh, 720px);
  padding: 0 var(--gutter);
  overflow: hidden;
  color: var(--ink);
  background-color: var(--bg);
}

.dot-hero__grid {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: repeat(16, 1fr);
  grid-template-rows: repeat(9, 1fr);
}

.dot-hero__dot {
  place-self: center;
  width: 60%;
  aspect-ratio: 1;
  border-radius: 50%;
  background-color: var(--ink);
}

.dot-hero__dot.is-hidden { visibility: hidden; }

.dot-hero__inner {
  position: relative; /* 背景の上に載せる */
  max-width: 34em;
}

.dot-hero__eyebrow {
  margin: 0;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.dot-hero__title {
  margin: 24px 0 0;
  font-size: clamp(40px, 8vw, 72px);
  line-height: 1.05;
  letter-spacing: -0.02em;
}

.dot-hero__lead {
  margin: 24px 0 0;
  font-size: 18px;
  line-height: 1.8;
}

.dot-hero__button {
  display: inline-grid;
  place-items: center;
  height: 48px;
  margin-top: 32px;
  padding: 0 32px;
  border-radius: 999px;
  background-color: var(--ink);
  color: var(--bg);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-decoration: none;
}

.dot-hero__button:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

JavaScript
import { gsap } from "gsap";

const COLS = 16;
const ROWS = 9;
const GAP = 0.5; // 波と波の間の静止時間(秒)
const CLEAR = 12; // 字の周りに点を置かない幅(px)
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

document.querySelectorAll(".dot-hero").forEach((hero) => {
  const grid = hero.querySelector(".dot-hero__grid");
  const inner = hero.querySelector(".dot-hero__inner");
  const style = getComputedStyle(hero);
  // 色は CSS 変数から読む。GSAP は var(--name) のままでは補間できないので、値にして渡す
  const ink = style.getPropertyValue("--ink").trim();
  const ripple = style.getPropertyValue("--ripple").trim();

  const dots = [];
  const fragment = document.createDocumentFragment();
  for (let i = 0; i < COLS * ROWS; i++) {
    const dot = document.createElement("span");
    dot.className = "dot-hero__dot";
    fragment.append(dot);
    dots.push(dot);
  }
  grid.append(fragment);

  // 字とボタンの後ろに来る点は隠す(波紋は隠れた点も同じ間合いで通る)
  function clearBehindText() {
    const boxes = [inner.querySelector(".dot-hero__button").getBoundingClientRect()];
    const walker = document.createTreeWalker(inner, NodeFilter.SHOW_TEXT);
    const range = document.createRange();
    while (walker.nextNode()) {
      range.selectNodeContents(walker.currentNode);
      boxes.push(...range.getClientRects());
    }
    const origin = grid.getBoundingClientRect();
    for (const dot of dots) {
      // 拡大したときの大きさで比べる(transform の影響を受けない offset で測る)
      const left = origin.left + dot.offsetLeft - CLEAR;
      const top = origin.top + dot.offsetTop - CLEAR;
      const right = left + dot.offsetWidth + CLEAR * 2;
      const bottom = top + dot.offsetHeight + CLEAR * 2;
      const hit = boxes.some((b) => b.width > 0 && left < b.right && right > b.left && top < b.bottom && bottom > b.top);
      dot.classList.toggle("is-hidden", hit);
    }
  }
  const observer = new ResizeObserver(clearBehindText);
  observer.observe(hero);
  observer.observe(inner);

  if (reduceMotion) {
    // 休んでいる小さな粒のまま止める(波紋は出さない)
    gsap.set(dots, { scale: 0.25, backgroundColor: ink });
    return;
  }

  // yoyo / repeat は stagger の中に書く(外に書くと、全体がまとめて往復してしまう)
  gsap.timeline({ repeat: -1, repeatDelay: GAP }).fromTo(
    dots,
    { scale: 0.25, backgroundColor: ink },
    {
      scale: 1,
      backgroundColor: ripple,
      duration: 0.35,
      ease: "sine.inOut",
      stagger: { each: 0.08, grid: [ROWS, COLS], from: "center", yoyo: true, repeat: 1 },
    }
  );
});

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

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