2026年5月23日
2026年5月23日
WordPressでResizeObserverを使ったレスポンシブコンポーネントの実装方法
ResizeObserver は、画面幅ではなく「要素そのものの大きさ」の変化を JavaScript で受け取れる API です。サイドバーに置いたときと本文幅に置いたときで振る舞いを変えたい部品や、親の幅に合わせて描き直す必要がある canvas のグラフなど、メディアクエリだけでは対応できない場面で役に立ちます。この記事では、WordPress のテーマに ResizeObserver を組み込む手順を、幅に応じて状態を切り替える汎用スクリプトと、canvas の描き直しの2つの例で説明し、ループエラーやレイアウトのずれを避けるコツもまとめます。
まず CSS のコンテナクエリで足りないか確認する
見た目(列数、文字サイズ、余白)を要素の幅で切り替えたいだけなら、今は CSS のコンテナクエリ(container-type と @container)が第一の選択肢です。主要ブラウザで使えるようになっており(2026年10月現在)、JavaScript の実行を待たずに最初の描画から正しいレイアウトになるため、表示のずれも起きません。
ResizeObserver が必要になるのは、次のように「大きさに応じて JavaScript の処理を変える」場面です。
- canvas や SVG のグラフを、表示幅に合わせて描き直す
- 幅が狭いときはタブをアコーディオンに切り替えるなど、構造や ARIA 属性まで変える
- 文章の高さを測って「続きを読む」ボタンを出すかどうか決める
- iframe の中身の高さを親ページに伝える
どちらか迷ったら、CSS だけで実現できるものは CSS で、計算や DOM 操作が要るものは ResizeObserver で、と分けると整理しやすくなります。
WordPress への組み込み方
スクリプトとスタイルの読み込み
子テーマの functions.php に追加します。親テーマを直接編集すると、テーマ更新時に変更が失われます。作業前にバックアップを取り、ステージング環境で確認してください。
<?php
add_action( 'wp_enqueue_scripts', function () {
$dir = get_stylesheet_directory_uri();
wp_enqueue_style( 'my-ro-components', $dir . '/css/ro-components.css', array(), '1.0.0' );
wp_enqueue_script(
'my-ro-components',
$dir . '/js/ro-components.js',
array(),
'1.0.0',
array( 'strategy' => 'defer', 'in_footer' => true )
);
} );
strategy 引数は WordPress 6.3 以降で使える書き方です。defer にしておくと、HTML の解析を止めずに読み込まれ、解析が終わってから実行されるので、スクリプトの中で要素を検索できます。
ブロックエディタで作ったグループブロックなどに目印を付けたい場合は、ブロックの設定サイドバーの「高度な設定」にある追加 CSS クラスの欄を使います(表記はバージョンによって異なることがあります)。ただし data-* 属性はこの欄では付けられないため、次の例のような属性はテンプレートやカスタム HTML ブロック、独自ブロックの出力で付けます。
例1:幅に応じて data 属性を切り替える汎用スクリプト
HTML 側では、切り替えの境目を data-ro-breakpoints に「名前:最小幅」の形で並べます。
<div class="product-cards" data-ro-breakpoints="sm:0, md:480, lg:768">
<!-- カード群 -->
</div>
( function () {
const targets = document.querySelectorAll( '[data-ro-breakpoints]' );
if ( ! targets.length || ! ( 'ResizeObserver' in window ) ) {
return;
}
const parse = ( el ) =>
el.dataset.roBreakpoints
.split( ',' )
.map( ( pair ) => pair.split( ':' ) )
.map( ( [ name, min ] ) => ( { name: name.trim(), min: Number( min ) } ) )
.sort( ( a, b ) => a.min - b.min );
const settings = new WeakMap();
// 1つの ResizeObserver で複数の要素をまとめて監視する
const observer = new ResizeObserver( ( entries ) => {
for ( const entry of entries ) {
const box = entry.contentBoxSize && entry.contentBoxSize[ 0 ];
const width = box ? box.inlineSize : entry.contentRect.width;
let current = '';
for ( const bp of settings.get( entry.target ) ) {
if ( width >= bp.min ) {
current = bp.name;
}
}
// 変化があったときだけ書き換える
if ( entry.target.dataset.roSize !== current ) {
entry.target.dataset.roSize = current;
entry.target.dispatchEvent( new CustomEvent( 'ro:change', { detail: { size: current, width } } ) );
}
}
} );
targets.forEach( ( el ) => {
settings.set( el, parse( el ) );
observer.observe( el );
} );
} )();
data-ro-size の値が変わると ro:change というカスタムイベントを発行するので、タブとアコーディオンの切り替えのような処理は、このイベントを受け取る別のスクリプトに書けます。見た目の切り替えは CSS で .product-cards[data-ro-size="lg"] { ... } のように書けますが、前述のとおり見た目だけならコンテナクエリのほうが適しています。この例は、JavaScript の処理と状態を共有したい場合の土台として使ってください。
contentBoxSize は配列で返るのが現在の仕様ですが、古いブラウザでは配列でなかったり存在しなかったりしたため、contentRect.width を予備として読んでいます。
例2:canvas のグラフを親の幅に合わせて描き直す
( function () {
const canvas = document.querySelector( '.my-chart canvas' );
if ( ! canvas || ! ( 'ResizeObserver' in window ) ) {
return;
}
const ctx = canvas.getContext( '2d' );
let frame = 0;
function draw( width, height ) {
ctx.clearRect( 0, 0, width, height );
ctx.fillStyle = '#3b82f6';
const values = [ 4, 7, 3, 9, 6 ];
const barWidth = width / values.length;
values.forEach( ( v, i ) => {
const h = ( v / 10 ) * height;
ctx.fillRect( i * barWidth + 4, height - h, barWidth - 8, h );
} );
}
const observer = new ResizeObserver( ( entries ) => {
const { width, height } = entries[ 0 ].contentRect;
cancelAnimationFrame( frame );
frame = requestAnimationFrame( () => {
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.round( width * dpr );
canvas.height = Math.round( height * dpr );
ctx.setTransform( dpr, 0, 0, dpr, 0, 0 );
draw( width, height );
} );
} );
observer.observe( canvas );
} )();
CSS 側では、次のように表示サイズを決めておきます。
.my-chart canvas {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
}
描画用のピクセル数(width 属性と height 属性)だけを JavaScript で変えるので、表示サイズは変わらず、監視と描画が互いに影響し合いません。devicePixelRatio を掛けているのは、高解像度の画面で線がぼやけないようにするためです。
つまずきやすい点
「ResizeObserver loop」のエラーが出る
コンソールに「ResizeObserver loop completed with undelivered notifications」というエラーが出ることがあります(ブラウザやバージョンによって文言は異なります)。コールバックの中で、監視している要素自身の大きさを変えると、同じフレーム内でさらに変化の通知が発生し、それを処理しきれなかったときに出るものです。
対処は次の順で検討します。
- コールバックで監視対象の大きさに影響する変更をしていないか見直す(例2のように、表示サイズは CSS で固定する)
- 値が変わったときだけ DOM を書き換える(例1の比較のように)
- 処理を
requestAnimationFrameで次のフレームに回す
境目付近で「狭いとき用の表示にすると幅が増え、広いとき用に戻ると幅が減る」という往復が起きる場合は、切り替えの境目に少し幅の余裕(ヒステリシス)を持たせると落ち着きます。
読み込み直後にレイアウトがずれる
ResizeObserver の最初の通知は、要素の描画後に届きます。初期状態の見た目と切り替え後の見た目が大きく違うと、読み込み直後に部品が動き、CLS(Cumulative Layout Shift)の悪化につながります。初期状態の CSS を一番よく使われる幅に合わせておく、見た目の切り替えはコンテナクエリに任せる、などの方法で、JavaScript による変化を小さく抑えます。アニメーションで切り替える場合は、prefers-reduced-motion で動きを減らす設定の利用者に配慮してください。
要素を取り除いたあとも監視が続く
Ajax で読み込んだ部品を差し替えるテーマやプラグインでは、取り除いた要素の監視が残ることがあります。要素を消す前に observer.unobserve( el )、部品ごと使わなくなったら observer.disconnect() を呼びます。後から追加される要素も監視したい場合は、MutationObserver と組み合わせる方法があります。
まとめ
- 見た目だけの切り替えは CSS のコンテナクエリ、計算や DOM 操作が要る処理は ResizeObserver、と使い分けます。
- スクリプトは子テーマから
wp_enqueue_script()で読み込み、WordPress 6.3 以降ならstrategyにdeferを指定します。 - 1つの ResizeObserver で複数の要素を監視し、値が変わったときだけ DOM を更新します。
- コールバックで監視対象の大きさを変えるとループエラーの原因になるため、表示サイズは CSS で固定し、必要なら
requestAnimationFrameで処理を後に回します。 - 初期表示との差を小さくして CLS を抑え、不要になった監視は
unobserve()やdisconnect()で解除します。