2026年7月30日

2026年7月30日

WordPressでCanvas要素を使って動的な背景アニメーションを実装する方法

WordPressサイトに動く背景を付けるなら、<canvas> をページ全体の後ろに固定し、JavaScriptで requestAnimationFrame を使って描画するのが基本の形です。この記事では、子テーマからスクリプトを正しく読み込む方法、粒子がゆっくり漂う背景アニメーションの実装、prefers-reduced-motion への対応と停止ボタン、表示されないときの確認ポイントまでを順に解説します。

コードはクラシックテーマ・ブロックテーマのどちらでも使えます。テーマ本体を直接編集すると更新で消えるため、子テーマ(またはサイト専用の小さなプラグイン)に追加し、ステージング環境で動きを確かめてから本番に反映してください。

全体の構成と考え方

背景アニメーションは「本文の邪魔をしない」ことが最優先です。そのため、次の方針で組み立てます。

  • canvas はJavaScriptで生成して body の先頭に入れる: テンプレートを書き換えずに済み、テーマを問いません。
  • position: fixed で画面全体に固定し、z-index を下げる: 通常のレイアウトの流れから外れるので、読み込み後に本文がずれる(CLS)原因になりにくくなります。
  • pointer-events: none と aria-hidden: クリックやタップを本文に通し、スクリーンリーダーには装飾として無視させます。
  • 動きを減らす設定の人には動かさない: OSの「視差効果を減らす」「アニメーションを減らす」系の設定は、CSS・JSの prefers-reduced-motion で読み取れます。
  • 見えていないときは止める: タブが裏に回ったときや停止ボタンが押されたときは描画ループを止め、CPUと電池の消費を抑えます。

ファイルは子テーマの中に次のように置く想定です。

  • functions.php(読み込みの設定)
  • assets/js/bg-canvas.js(描画のスクリプト)

子テーマからスクリプトとCSSを読み込む

header.php に <script> を直書きする方法もありますが、ブロックテーマにはそのファイルがありませんし、キャッシュ系プラグインとの相性も悪くなります。WordPressの作法どおり wp_enqueue_script で読み込みます。

<?php
// 子テーマの functions.php に追加
add_action( 'wp_enqueue_scripts', function () {
	// 背景を動かすのはトップページだけにする(全ページにしたい場合はこの条件を外す)
	if ( ! is_front_page() ) {
		return;
	}

	$rel  = '/assets/js/bg-canvas.js';
	$path = get_stylesheet_directory() . $rel;

	wp_enqueue_script(
		'wpdr-bg-canvas',
		get_stylesheet_directory_uri() . $rel,
		array(),
		file_exists( $path ) ? (string) filemtime( $path ) : null,
		array(
			'in_footer' => true,
			'strategy'  => 'defer',
		)
	);

	// ファイルを持たないスタイルを登録し、インラインCSSだけを出力する
	wp_register_style( 'wpdr-bg-canvas', false, array(), null );
	wp_enqueue_style( 'wpdr-bg-canvas' );
	wp_add_inline_style( 'wpdr-bg-canvas', '
		.wpdr-bg-canvas {
			position: fixed;
			inset: 0;
			width: 100%;
			height: 100%;
			z-index: -1;
			pointer-events: none;
			display: block;
		}
		.wpdr-bg-toggle {
			position: fixed;
			right: 16px;
			bottom: 16px;
			z-index: 10;
			padding: 0.5em 0.9em;
			font-size: 0.85rem;
		}
	' );
} );

ポイントは3つです。

1つ目は、第5引数の配列で 'strategy' => 'defer' を指定している点です。この書き方はWordPress 6.3で追加されたもので、HTMLの解析を止めずにスクリプトを読み込めるため、表示の遅れ(レンダリングを妨げるリソース)を増やしません。古いバージョンでは配列ではなく真偽値(フッターに出すかどうか)しか受け付けないので、まずWordPressを更新しておきます。

2つ目は、バージョン番号にファイルの更新時刻を使っている点です。JSを書き換えるたびにURLのクエリが変わるので、ブラウザやCDNの古いキャッシュが残って「直したのに反映されない」という状況を避けられます。

3つ目は、CSSを wp_add_inline_style で出している点です。数行のCSSのために別ファイルを増やさずに済みます。子テーマの style.css に書いても構いません。

canvasで粒子の背景を描く

次に assets/js/bg-canvas.js を作ります。小さな円がゆっくり漂うだけのシンプルな背景です。

(function () {
  'use strict';

  var canvas = document.createElement('canvas');
  canvas.className = 'wpdr-bg-canvas';
  canvas.setAttribute('aria-hidden', 'true');
  document.body.prepend(canvas);

  var ctx = canvas.getContext('2d');
  if (!ctx) {
    return;
  }

  var motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
  var width = 0;
  var height = 0;
  var rafId = 0;
  var last = 0;
  var paused = false;
  var particles = [];

  // 画面サイズと解像度に合わせて canvas の実ピクセル数を決める
  function resize() {
    var dpr = Math.min(window.devicePixelRatio || 1, 2); // 高解像度端末でも2倍までに抑える
    width = window.innerWidth;
    height = window.innerHeight;
    canvas.width = Math.round(width * dpr);
    canvas.height = Math.round(height * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }

  // 粒子の数は画面の面積に比例させ、上限を80個にする
  function createParticles() {
    var count = Math.min(80, Math.round((width * height) / 18000));
    particles = [];
    for (var i = 0; i < count; i++) {
      particles.push({
        x: Math.random() * width,
        y: Math.random() * height,
        r: 1 + Math.random() * 2,
        vx: (Math.random() - 0.5) * 0.4,
        vy: (Math.random() - 0.5) * 0.4
      });
    }
  }

  function draw() {
    ctx.clearRect(0, 0, width, height);
    ctx.fillStyle = 'rgba(70, 110, 190, 0.35)';
    for (var i = 0; i < particles.length; i++) {
      var p = particles[i];
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function step(now) {
    // 経過時間で動かすので、60Hzでも120Hzの画面でも速さが変わらない
    var dt = Math.min((now - last) / 16.67, 3);
    last = now;
    for (var i = 0; i < particles.length; i++) {
      var p = particles[i];
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      if (p.x < -5) { p.x = width + 5; }
      if (p.x > width + 5) { p.x = -5; }
      if (p.y < -5) { p.y = height + 5; }
      if (p.y > height + 5) { p.y = -5; }
    }
    draw();
    rafId = window.requestAnimationFrame(step);
  }

  function shouldAnimate() {
    return !paused && !motionQuery.matches && !document.hidden;
  }

  // 状態が変わるたびに呼び、動かすか止めるかを決める
  function update() {
    if (shouldAnimate()) {
      if (!rafId) {
        last = performance.now();
        rafId = window.requestAnimationFrame(step);
      }
    } else {
      if (rafId) {
        window.cancelAnimationFrame(rafId);
        rafId = 0;
      }
      draw(); // 止めている間は静止画を1枚だけ表示する
    }
    toggle.hidden = motionQuery.matches;
  }

  // 停止・再生ボタン
  var toggle = document.createElement('button');
  toggle.type = 'button';
  toggle.className = 'wpdr-bg-toggle';
  toggle.textContent = '背景アニメーションを停止';
  toggle.addEventListener('click', function () {
    paused = !paused;
    toggle.textContent = paused ? '背景アニメーションを再生' : '背景アニメーションを停止';
    update();
  });
  document.body.appendChild(toggle);

  var resizeTimer = 0;
  window.addEventListener('resize', function () {
    window.clearTimeout(resizeTimer);
    resizeTimer = window.setTimeout(function () {
      resize();
      draw();
    }, 150);
  });

  document.addEventListener('visibilitychange', update);
  motionQuery.addEventListener('change', update);

  resize();
  createParticles();
  update();
})();

なぜこう書くのか

devicePixelRatio を掛けて上限を2にする: canvasは指定したピクセル数で描画されるため、高解像度の画面でCSSのサイズのまま描くとぼやけます。一方で3倍・4倍の端末にそのまま合わせると、描くピクセル数が大きく増えて重くなります。背景用途なら2倍までで見た目は十分です。

経過時間(dt)で動かす: requestAnimationFrame の呼ばれる回数は画面のリフレッシュレートによって変わります。1回ごとに一定量動かす書き方だと、120Hzの端末では倍速に見えます。前回からの経過時間を掛けることで、どの端末でも同じ速さになります。

リサイズ時に粒子を作り直さない: スマートフォンではスクロールでアドレスバーが出入りするたびに resize が発生します。そのたびに粒子を作り直すと背景が飛んで見えるため、canvasのサイズだけを合わせ、はみ出した粒子は画面の反対側に回り込ませています。

粒子同士を線で結ぶ表現は控えめに: よく見かける「近い粒子を線で結ぶ」表現は、全組み合わせの距離を毎フレーム計算するため、粒子数の2乗で処理が増えます。取り入れる場合は粒子数を少なめにし、古いスマートフォンで動きが滑らかかを確認してください。描画が重くなる場合は、Web Worker と OffscreenCanvas でメインスレッドから処理を逃がす方法もあります(ブラウザの対応状況は事前に確認が必要です)。

動きを減らす設定とアクセシビリティ

背景アニメーションは装飾ですが、動くものが苦手な人や、前庭障害のある人にとっては読書の妨げになります。今回のコードでは2段構えで配慮しています。

  • prefers-reduced-motion: OS側で動きを減らす設定をしている人には、最初から静止画を表示し、停止ボタンも出しません。設定が途中で変わった場合も change イベントで追従します。
  • 停止ボタン: W3Cの勧告であるWCAG 2.2(2026年10月現在)の達成基準2.2.2「一時停止、停止、非表示」(レベルA)は、自動で始まり5秒より長く続き、ほかのコンテンツと並んで表示される動きについて、利用者が止められる仕組みを求めています。背景の動きも対象になり得るため、止める手段を用意しておくのが安全です。

もう1つ見落としやすいのが文字のコントラストです。背景に色の付いた粒子が流れると、場所によって本文の文字が読みにくくなります。粒子の色を薄くする、本文のエリアに半透明の背景色を敷く、といった方法で、どの瞬間でも文字が読めるかを確認してください。自動チェックツールは動く背景の上のコントラストまでは正しく判定できないことが多いので、目視での確認も欠かせません。

表示されない・重いときの確認ポイント

実装後によくある症状と、確認する順番をまとめます。

  1. canvasがそもそもない: ブラウザの開発者ツールで要素を検索し、wpdr-bg-canvas があるかを見ます。無ければスクリプトが読み込まれていません。ネットワークタブで bg-canvas.js が404になっていないか、is_front_page() の条件で除外されていないかを確認します。キャッシュ系プラグインやCDNを使っている場合は、キャッシュを削除してから確かめます。
  2. canvasはあるのに見えない: テーマによっては、ページ全体を包む要素(ブロックテーマなら .wp-site-blocks など)に背景色が付いていて、その下に隠れていることがあります。開発者ツールで背景色を持つ要素を探し、透明にするか、canvasを特定のセクションの中に置く形に変えます。
  3. JavaScriptのエラー: コンソールにエラーが出ていないかを見ます。最適化プラグインでJSの結合や遅延読み込みをしている場合、実行順が変わって動かないことがあるので、このファイルを対象から外して試します。
  4. スクロールが重い: パフォーマンスタブで記録し、描画処理(step)に時間がかかっていないかを見ます。粒子数を減らす、devicePixelRatio の上限を1.5に下げる、といった調整が効果的です。
  5. ボタンが他の要素と重なる: 右下に固定のチャットボタンや「トップへ戻る」ボタンがあるテーマでは位置が重なります。.wpdr-bg-toggle の right や bottom を調整してください。

最後に、PageSpeed Insights などで表示速度を測り、導入前と比べて大きく悪化していないかを確認します。スクリプトは defer で読み込んでいるので、初回表示を妨げることは少ないはずですが、ほかのスクリプトとの組み合わせで結果は変わります。

まとめ

  • canvasはJavaScriptで生成し、position: fixed と z-index で本文の後ろに固定すると、テーマを問わず使え、レイアウトのずれも起きにくくなります。
  • スクリプトは子テーマから wp_enqueue_script で読み込み、WordPress 6.3以降の strategy 引数で defer を指定します。
  • 描画は requestAnimationFrame と経過時間で行い、解像度の上限と粒子数の上限で重さを抑えます。
  • prefers-reduced-motion と停止ボタンで、動きが苦手な人に配慮します。本文のコントラストも目で確認してください。
  • 表示されないときは「要素があるか」「背景色に隠れていないか」「JSエラーがないか」の順に確認します。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲