ResizeObserver の使い方
ResizeObserver の基本的な使い方や詳細についての解説のような覚書です。ResizeObserver を使用すると特定の要素のサイズ変更を簡単に検出して何らかの処理を実行する(コールバック関数を呼び出す)ことができます。
作成日:2023年1月6日
ResizeObserver とは
ResizeObserver は要素の矩形領域のサイズの変化を監視して、そのサイズに変化があるとコールバック関数を呼び出すオブジェクトです。
window.resize イベントではウィンドウサイズ(ビューポート)の変更を検知しますが、ResizeObserver を使用すると特定の要素のサイズ変更を簡単に検出できます。
以下はテキストエリアの幅と高さを検出してそれらの値を出力し、幅の値により背景色を変更する例です。
textarea 要素は右下の隅にドラッグするためのハンドルがあり、大きさを変更することができます。
width: px height: px
以下は上記サンプルの HTML と JavaScript です。
<textarea></textarea> <p> width: <span id="w"></span>px height: <span id="h"></span>px </p>
window.resize イベントを利用するのに比べて簡単に要素の幅の変化を検出することができます。
const w = document.getElementById('w'); //width を出力する span 要素 const h = document.getElementById('h'); //height を出力する span 要素 //コンストラクターにコールバック関数を渡してオブザーバーを生成 const observer = new ResizeObserver( entries => { //変更後のサイズの情報(entries)は配列になっているので for of などで反復処理 for (let entry of entries) { //変更後の width の値 const width = entry.contentRect.width; //変更後の height の値 const height = entry.contentRect.height; //変更が発生した要素(この場合は textarea 要素) const targetElem = entry.target; w.textContent = width; // span 要素に width を出力 h.textContent = height; // span 要素に height を出力 //幅の値により背景色を変更 if(width > 400) { targetElem.style.backgroundColor = 'pink'; }else{ targetElem.style.backgroundColor = 'yellow'; } } }); //オブザーバーの observe() メソッドに監視対象の要素を指定して監視を開始 observer.observe(document.querySelector('textarea'));
現時点では主要なブラウザのほどんどで ResizeObserver API を利用することができます。
ResizeObserver 関連リンク
- MDN: ResizeObserver
- web.dev: 要素の document.onresize のような ResizeObserver
- Editor’s Draft, 4 October 2022 Resize Observer
その他の Observer のページ
ResizeObserver の使い方
ResizeObserver を利用するには、コンストラクターでオブザーバー(ResizeObserver オブジェクトのインスタンス)を生成し、observe() メソッドを使ってサイズ変更の監視を開始します。
コンストラクターには、サイズ変更を検知した際に実行するコールバック関数を指定し、observe() メソッドには監視対象の要素を指定します。
以下は ResizeObserver の基本的な使い方です。
オブザーバーを生成
ResizeObserver を利用するには、コンストラクター ResizeObserver()
にコールバック関数を渡して ResizeObserver オブジェクトのインスタンス(オブザーバー)を生成します。
コールバック関数にはサイズ変更を検知した(サイズが変更された)際に実行する処理を記述します。
// オブザーバーを生成して変数 myObserver に代入 const myObserver = new ResizeObserver( (entries, observer) => { //サイズ変更を検知した際に実行する処理 });
上記は以下のようにコールバック関数を別途定義して記述することもできます。
//コールバック関数を別途定義 const callback = (entries, observer) => { //サイズ変更を検知した際に実行する処理 } // コールバック関数を渡してオブザーバーを生成 const myObserver = new ResizeObserver(callback);
コールバック関数の定義
コールバック関数は以下の2つの引数を受け取ります。
- 第一引数 entries: ResizeObserverEntry オブジェクトの配列
- 第二引数 observer: ResizeObserver 自身への参照(省略可能)
第一引数の entries
は ResizeObserverEntry オブジェクトの配列
です。配列になっているのは、ResizeObserver は複数の要素を監視することができるので、同時に複数のサイズ変更が発生する可能性があるためです。
一般的には、for of
や forEach
などを使って配列を反復処理します。
配列の個々の要素(以下では entry
)には発生した変更に関する情報(ResizeObserverEntry オブジェクト)が渡されます。
const myObserver = new ResizeObserver( (entries) => { // for of を使って ResizeObserverEntry オブジェクトの配列を反復処理 for (let entry of entries) { // 各 ResizeObserverEntry オブジェクトを使った処理 } });
ResizeObserverEntry オブジェクトは変更後の新しいサイズと変更が発生した要素の参照がセットされたプロパティを持つオブジェクトです。
以下では要素の参照がセットされた target
プロパティからタグ名(tagName
)を、新しいサイズがセットされた contentRect
プロパティから幅(width
)と高さ(height
)を取得しています。
const myObserver = new ResizeObserver( (entries) => { for (let entry of entries) { //サイズ変更が発生した要素(target プロパティ)のタグ名(tagName) console.log('Target: ' + entry.target.tagName); //変更後の新しいサイズ(contentRect)の幅(width) console.log('width:' + entry.contentRect.width); //変更後の新しいサイズ(contentRect)の高さ(height) console.log('height:' + entry.contentRect.height); } });
const myObserver = new ResizeObserver( (entries) => { entries.forEach( (entry) => { console.log('Target: ' + entry.target.tagName); console.log('width:' + entry.contentRect.width); console.log('height:' + entry.contentRect.height); }); });
監視対象の要素が1つだけの場合
監視対象の要素が1つだけの場合は、第一引数の entries
を for of
などを使って反復処理するのではなく、entries
の最初の要素 entries[0]
にアクセスして処理することもできます。
const myObserver = new ResizeObserver( (entries) => { console.log('Target: ' + entries[0].target.tagName); console.log('width:' + entries[0].contentRect.width); console.log('height:' + entries[0].contentRect.height); });
以下のように配列の長さと要素を確認しても良いかもしれませんが、冗長かもしれません。
const myObserver = new ResizeObserver( (entries) => { if(entries.length > 0 && entries[0]) { console.log('Target: ' + entries[0].target.tagName); console.log('width:' + entries[0].contentRect.width); console.log('height:' + entries[0].contentRect.height); } });
以降では基本的には for of
を使って entries
の配列を反復処理しています。
observe() で監視を開始
observe()
メソッドに監視対象の要素を指定して実行することで監視を開始します。observe()
は ResizeObserver オブジェクト(オブザーバー)のメソッドです。
observe()
を実行すると、オブザーバーは監視対象の要素のサイズが変更される度にコールバック関数に変更の情報を渡して呼び出します(指定した処理を実行します)。
以下の例では監視の対象をテキストエリア(textarea 要素)としています。
※ observe()
を実行するには、引数に指定する監視対象の要素が HTML 上に存在する必要があります。以下の場合、HTML 上に <textarea>
が存在しないとエラーになります。
const myObserver = new ResizeObserver( (entries) => { for (let entry of entries) { console.log('Target: ' + entry.target.tagName); console.log('width:' + entry.contentRect.width); console.log('height:' + entry.contentRect.height); } }); // 監視対象の要素(テキストエリア)を取得して変数に代入 const targetElem = document.querySelector('textarea'); // observe() に監視対象の要素を指定して監視を開始 myObserver.observe(targetElem); // または myObserver.observe(document.querySelector('textarea'));
<textarea></textarea>
上記では監視対象にテキストエリアを指定したので、テキストエリアのサイズが変更される度に、コールバック関数に指定した処理(コンソールにタグ名と幅、高さを出力)が実行されます。
変更後のサイズの取得
要素の変更後のサイズは、コールバック関数の引数に渡される ResizeObserverEntry オブジェクトの以下のプロパティから取得することができます。
前述の例では要素の変更後のサイズ(幅と高さ)を contentRect
から取得しましたが、以下は contentBoxSize
から取得する例です。
contentBoxSize
プロパティは配列になっていて、最初の要素 contentBoxSize[0]
の inlineSize
に幅が、blockSize
に高さがセットされます。
以下ではサイズ変更が発生する度に幅と高さを取得して、それらの値を使ってグラデーションの角度や色、表示するテキストやその文字色を変化させています。変数 diff
はグラデーションを変化させるための適当に算出した値です。
const ro = new ResizeObserver(entries => { for (let entry of entries) { //contentBoxSize プロパティから幅を取得 const width = entry.contentBoxSize[0].inlineSize; //contentBoxSize プロパティから高さを取得 const height = entry.contentBoxSize[0].blockSize; //グラデーションの値を調整するための適当な値 const diff = Math.round((width + height) / 7); //幅と高さによりグラデーションを変化させる entry.target.style.backgroundImage = ` linear-gradient( ${width > height ? 90 : 180}deg, royalblue ${-80 - diff}%, pink ${40 - diff}%, palegoldenrod ${80 - diff}%, mediumaquamarine ${180 - diff}%, blue ${300 - diff}% )`; //テキストを表示する p 要素を取得 const p = entry.target.querySelector('p'); //幅と高さの値により表示するテキストを変更 if (width > 200 && height > 20 || width > 100 && height > 40) { p.textContent = `width: ${width}px height: ${height}px`; } else { //テキストを表示するのに十分なサイズがなければテキストを空に p.textContent = ''; } //サイズ(幅と高さの合計)により文字色を切り替え if(width + height > 1000) { p.style.color = '#fff'; } else { p.style.color = '#333'; } } }); ro.observe(document.querySelector('.gradient.resizable'));
以下の div 要素の右下端をドラッグしてサイズ変更すると、背景色のグラデーションが変化します。
<div class="gradient resizable"> <p></p> </div><span class="red-arrow">←</span>
.resizable { /* 垂直及び水平方向のリサイズを許可 */ resize: both; /* visible 以外を指定 */ overflow: auto; } .gradient { width: 200px; height: 50px; min-width: 40px; min-height: 20px; margin: 20px 0; border: 1px solid #ccc; font-size: 14px; /* p 要素の基準に */ position: relative; /* span 要素を横並びに */ display: inline-block; /* 背景にリニアグラテーションを適用 */ background-image: linear-gradient( royalblue -80%, /* 始点 */ pink 40%, palegoldenrod 80%, mediumaquamarine 180%, blue 300% /* 終点 */ ); } .gradient p { margin: 0; padding: 0; /* 垂直及び水平方向に中央寄せ */ position: absolute; top: 50%; left: 50%; margin-right: -50%; transform: translate(-50%, -50%); } .red-arrow { color: red; display: inline-block; transform: translateY(-8px) rotate(45deg); }
関連項目:グラデーションのアニメーション(Intersection Observer API の使い方)
古いブラウザへの対応
現時点での主要なブラウザのほどんどで ResizeObserver API を利用することができますが、古いブラウザでは利用できません(caniuse)。
また、変更後の新しいサイズを表すプロパティで最も広くサポートされているのが contentRect ですが、将来的には非推奨になる可能性があります。
前述の例のように contentRect
の代わりに contentBoxSize
を使用することができます。但し、古い Firefox では contentBoxSize
が配列でないものがあります(caniuse)。
以下は ResizeObserver が利用できるかを判定し、続いて contentBoxSize
が利用できるかを判定し、利用できれなければ contentRect
を利用する例です。
以下のスクリプトでは、リサイズオブザーバーを使用して監視対象の div 要素の幅が変更されると h3 要素と p 要素の font-size を変更しています(参考:MDN ResizeObserver)。
//ResizeObserver が利用できれば if (window.ResizeObserver) { // 変更を監視する対象の要素 const target = document.querySelector('.target'); // 対象の要素の子要素の h3 要素を取得 const h3s = target.querySelectorAll('h3'); // 対象の要素の子要素の p 要素を取得 const ps = target.querySelectorAll('p'); const resizeObserver = new ResizeObserver(entries => { for (let entry of entries) { // contentBoxSize が利用できれば if (entry.contentBoxSize) { // contentBoxSize が標準の配列であれば if (entry.contentBoxSize[0]) { h3s.forEach((h3) => { //フォントサイズを幅から算出した値と 1.5 のどちらか大きい値を設定 h3.style.fontSize = Math.max(1.5, entry.contentBoxSize[0].inlineSize / 300) + 'rem'; }); ps.forEach((p) => { p.style.fontSize = Math.max(1, entry.contentBoxSize[0].inlineSize / 600) + 'rem'; }); } else { // 古い Firefox は配列ではないので h3s.forEach((h3) => { h3.style.fontSize = Math.max(1.5, entry.contentBoxSize.inlineSize / 300) + 'rem'; }); ps.forEach((p) => { p.style.fontSize = Math.max(1, entry.contentBoxSize.inlineSize / 600) + 'rem'; }); } } else { // contentBoxSize が利用できなければ contentRect を使用 h3s.forEach((h3) => { h3.style.fontSize = Math.max(1.5, entry.contentRect.width / 300) + 'rem'; }); ps.forEach((p) => { p.style.fontSize = Math.max(1, entry.contentRect.width / 600) + 'rem'; }); } } }); resizeObserver.observe(target); } else { console.log('ResizeObserver がサポートされていません。'); }
<div class="target"> <h3>Magni ipsum nam</h3> <p>Lorem ipsum, dolor sit amet consectetur adipisicing elit...</p> <p>Excepturi, sequi nesciunt sint quam rem vero nam cum ....</p> <p>Sed similique expedita cumque dolorem nihil atque,...</p> </div>
.target { resize: both; overflow: auto; padding: 1rem; margin: 1rem 0; border: 1px solid #999; width: 95%; background-color: #effbf1; } .target p { max-width: 780px; }
ブラウザの幅を変更するか、右下のハンドルをドラッグしてサイズを変更すると、フォントサイズも変更されます。
Magni ipsum nam
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Magni ipsum nam, esse debitis alias exercitationem quibusdam amet unde dolores vero placeat facere doloremque sunt dolorem, quidem officiis quisquam reiciendis. Asperiores?
Excepturi, sequi nesciunt sint quam rem vero nam cum perspiciatis, cumque ab suscipit a dolores placeat corrupti unde consequatur distinctio. Tenetur autem delectus et voluptatem culpa dolor maxime voluptate possimus.
Sed similique expedita cumque dolorem nihil atque, nesciunt quasi provident cum odio itaque. In eaque ducimus odit velit aut, cum quaerat facilis ad totam nihil deserunt tempore, repudiandae, provident temporibus!
複数の要素を監視
オブザーバーは observe()
に異なる監視対象を指定して複数の要素を監視することができます。
// オブザーバーを生成 const ro = new ResizeObserver( (entries) => { //省略 }); // #foo の監視を開始 ro.observe(document.querySelector('#foo')); // #bar の監視を開始 ro.observe(document.querySelector('#bar'));
以下は複数の要素のサイズ変更(変化)を監視する例です。
次のような HTML がある場合に複数の要素(h3、p、textarea 要素)を監視して、変更を検知すると要素ごとに異なる処理を実行します。
また、確認用に、同時に発生(検知)したサイズ変更の数を出力します。
<h3>Default Title</h3> <p style="border: 1px dashed green; max-width: 680px;"></p> <textarea style="width: 70%; min-width: 300px;"></textarea> <p>同時に発生したサイズ変更の数: <span id="count"></span></p>
コールバック関数では最初に発生したサイズ変更の数(entries.length
)を取得して出力しています。
そして for of
で個々の変更情報(entry
)の target
プロパティで変更が発生した要素を取得し、contentRect
プロパティでその要素の変更後の幅(width
)を取得して変数に代入しています。
そして、変更が発生した要素のタグ名(tagName
)を調べて、要素ごとに処理を分岐しています。
※この例では要素を特定するのに tagName
を使用していますが、id
や class
属性などが指定してあれば、それらを使って要素を特定することができます(例:entry.target.id
)。
h3
要素では、幅の値によりテキストと背景色を切り替えています。以下では幅 600px で切り替えていますが、contentRect
の場合、幅の値 600px にはパディングは含まれません。
また、h3
要素のテキストはページを読み込んだ時点での h3
要素の幅により、以下のスクリプトで書き換えられるため、HTML に記述してある Default Title というテキストは表示されません。
p
要素では、直前の幅の値と変更後の幅の値を比較してテキストの内容を切り替えています。
textarea
要素では、幅の値により背景色を切り替えています。
最後に生成したオブザーバーの observe()
メソッドに監視対象の要素をそれぞれ指定して、それぞれの要素の監視を開始しています。
// p 要素の変更前の width を保存する変数 let p_prev_width; //同時に発生したサイズ変更の数の出力先の要素 const countSpan = document.getElementById('count'); const ro = new ResizeObserver( (entries) => { //発生したサイズ変更の数 const count = entries.length; //サイズ変更の数を出力 countSpan.textContent = count; for (let entry of entries) { // 変更された要素 const targetElem = entry.target; // 変更された要素の幅 const width = entry.contentRect.width; // 変更された要素が h3 要素の場合 if(targetElem.tagName === 'H3'){ if(width >= 600) { targetElem.textContent = '幅は 600px 以上です'; targetElem.style.backgroundColor = 'lightblue'; }else{ targetElem.textContent = '幅は 600px 未満です'; targetElem.style.backgroundColor = 'yellow'; } } // 変更された要素が p 要素の場合 if(targetElem.tagName === 'P'){ // p_prev_width の値がまだ無い場合は width を代入(初回) if(!p_prev_width) p_prev_width = width; if( p_prev_width < width) { targetElem.textContent = '増加しました。'; }else if (p_prev_width === width) { targetElem.textContent = 'まだ変更されていません'; }else{ targetElem.textContent = '減少しました。'; } // p_prev_width の値を更新 p_prev_width = width; } // 変更された要素が textarea 要素の場合 if(targetElem.tagName === 'TEXTAREA'){ //幅が300px未満では背景色は透明、400px以上では背景色をピンクに targetElem.style.backgroundColor = width < 400 ? 'transparent': 'pink'; } } }); // h3 要素の監視を開始 ro.observe(document.querySelector('h3')); // p 要素の監視を開始 ro.observe(document.querySelector('p')); // textarea要素の監視を開始 ro.observe(document.querySelector('textarea'));
画面幅を変更すると、h3
要素と p
要素の幅も変更されます。但し、p
要素には max-width: 680px
が指定してあるので、テキストが切り替わるのは680px以下の場合になります。
textarea
要素は右下をドラッグしてサイズを変更することができます。
「同時に発生したサイズ変更の数」はその時のブラウザの幅や textarea
要素をドラッグしたかどうなかど状況により変わります。
ResizeObserver Sample
同時に発生したサイズ変更の数:
まとめて observe() を実行
observe()
の引数には要素の配列やノードリスト(NodeList)を指定することができません。
前述の例のように、observe()
に個別に監視対象の要素を指定することができますが、以下は対象の要素を querySelectorAll()
で取得し、forEach()
を使ってまとめて observe()
に指定する例です。
<div class="ro-target foo">Foo</div> <div class="ro-target bar">Bar</div> <div class="ro-target baz">Baz</div>
コールバック関数では、前述の例同様、target
プロパティで変更が発生した要素を取得し、contentRect
プロパティでその要素の変更後の幅を取得して変数に代入しています。
この例では変更が発生した要素に特定のクラスが指定されているかを classList.contains()
で調べて要素を特定しています。
対象の要素は ro-target
クラスが指定されている要素をまとめて querySelectorAll() で取得し、取得したノードリスト(NodeList)で forEach()
を使ってまとめて observe()
に指定しています。
const ro = new ResizeObserver( (entries) => { for (let entry of entries) { // 変更された要素 const targetElem = entry.target; // 変更後の幅を取得 const width = entry.contentRect.width; //サイズ変更が発生した要素のクラスに foo が含まれていれば if(targetElem.classList.contains('foo')) { if(width > 560) { targetElem.style.backgroundColor = 'yellow'; }else{ targetElem.style.backgroundColor = 'transparent'; } } //サイズ変更が発生した要素のクラスに bar が含まれていれば if(targetElem.classList.contains('bar')) { if(width > 600) { targetElem.style.backgroundColor = 'pink'; }else{ targetElem.style.backgroundColor = 'transparent'; } } //サイズ変更が発生した要素のクラスに baz が含まれていれば if(targetElem.classList.contains('baz')) { if(width > 640) { targetElem.style.backgroundColor = 'lightblue'; }else{ targetElem.style.backgroundColor = 'transparent'; } } } }); //対象の要素(.ro-target)をまとめて querySelectorAll() で取得 const targetElements = document.querySelectorAll('.ro-target'); //forEach() を使ってまとめて observe() を実行 targetElements.forEach((elem) => { ro.observe(elem); });
監視の停止(解除)
開始したサイズ変更の監視を停止するには、unobserve() メソッドを使って監視対象の要素を指定して個別に監視を停止する方法と、disconnect() メソッドを使って全ての監視を停止する方法があります。
対象ごとに監視を停止
ResizeObserver オブジェクト(インスタンス)のメソッド unobserve()
に対象の要素を指定して、その要素の監視を停止することができます。
コールバック関数内で unobserve()
を使用する場合は、コールバック関数の第二引数に渡されるオブザーバー自身への参照(observer
)を利用することができます。
以下は id="foo"
と id="bar"
の2つの要素のサイズ変更をそれぞれ監視して、id="foo"
の変更の回数が50に達したら、id="foo"
の監視を停止(終了)します。
この例では、変更後の幅の値をもとに要素の角丸のスタイルを変更しています。border-radius
に指定する値は Math.max()
を使って width
の値をもとに適当に算出しています。
//変更を検知した回数を格納する変数 let countForFoo = 0; let countForBar = 0; //コールバック関数の第二引数にオブザーバーの参照を受け取る const ro = new ResizeObserver( (entries, observer) => { for (let entry of entries) { //サイズ変更後の幅を取得 const width = entry.contentBoxSize[0].inlineSize; //要素の角丸のスタイルを変更後の幅の値をもとに更新 entry.target.style.borderRadius = Math.max(0, (width - 500) / 5) + 'px'; //サイズ変更された要素が #foo であれば if (entry.target.id === 'foo') { countForFoo ++; entry.target.querySelector('span').textContent = countForFoo; //変更を検知した回数が50になったら if (countForFoo >= 50) { //unobserve() に対象の要素を指定して監視を終了(observer は第二引数の自身への参照) observer.unobserve(entry.target); entry.target.innerHTML = `監視を終了しました。変更回数:<span>${countForFoo}</span>`; } } //サイズ変更された要素が #bar であれば if (entry.target.id === 'bar') { countForBar ++; entry.target.querySelector('span').textContent = countForBar; } } }); ro.observe(document.querySelector('#foo')); ro.observe(document.querySelector('#bar'));
<div id="foo">変更検知50回で監視を停止。変更回数:<span></span></div> <div id="bar">変更回数:<span></span></div>
要素の幅が変更されると、それに伴い角丸の値が変わります。id="foo"
の要素(ピンク色の背景)は変更回数が50になると、監視を停止するので、その時点からカウントと角丸の値は変わりません。id="bar"
の要素(水色の背景)は監視は継続されるので、幅の変更に伴いカウントと角丸が変化し続けます。
また、カウントの初期値は0ですが、ページを読み込んだ時点で変更が検知されてカウントアップされるため、表示される変更回数は1になります。
全ての監視を解除
ResizeObserver オブジェクト(インスタンス)のメソッド disconnect() を使うと、全ての監視をまとめて解除することができます。
以下は id="foo"
と id="bar"
の2つの要素のサイズ変更をそれぞれ監視して、id="foo"
の変更の回数が50に達したら、disconnect()
を使って2つの要素の監視をまとめて解除(停止)します。
unobserve()
同様、コールバック関数内で disconnect()
を使用する場合は、コールバック関数の第二引数に渡されるオブザーバー自身への参照(observer
)を利用することができます。
この例では監視を停止・再開するボタンを追加しています。
<div id="foo">変更検知50回以上で監視を停止。変更回数:<span></span></div> <div id="bar">変更回数:<span></span></div> <button>Stop</button>
コールバック関数内では、第二引数の observer
を使って observer.disconnect()
で監視を解除できますが、ボタン(コールバック関数の外側)で監視を解除する際は、生成したオブザーバーを格納した変数 ro
を使って ro.disconnect()
で監視を解除します。
監視を再開するには、再度 observe()
に監視対象を指定して実行します。
//変更を検知した回数を格納する変数 let countForFoo = 0; let countForBar = 0; //監視中かどうかのフラグ let isObserved = true; //コールバック関数の第二引数にオブザーバーの参照を受け取る const ro = new ResizeObserver( (entries, observer) => { for (let entry of entries) { const width = entry.contentBoxSize[0].inlineSize; entry.target.style.borderRadius = Math.max(0, (width - 500) / 5) + 'px'; if (entry.target.id === 'foo') { countForFoo ++; entry.target.querySelector('span').textContent = countForFoo; if (countForFoo >= 50) { //disconnect() で全ての要素の監視を解除(observer は第二引数の自身への参照) observer.disconnect(); entry.target.innerHTML = `変更検知50回で監視を停止。変更回数:<span>${countForFoo}</span>`; //ボタンをクリックしてボタンのラベルを変更して isObserved を false に document.querySelector('button').click(); } } if (entry.target.id === 'bar') { countForBar ++; entry.target.querySelector('span').textContent = countForBar; } } }); const foo = document.querySelector('#foo'); const bar = document.querySelector('#bar'); //#foo の監視を開始 ro.observe(foo); //#bar の監視を開始 ro.observe(bar); //監視を停止・再開するボタンのイベントリスナーを設定 document.querySelector('button').addEventListener('click', (e)=> { if(isObserved) { //disconnect() で全ての監視を停止 ro.disconnect(); //監視中かどうかのフラグを変更(監視停止中) isObserved = false; if(countForFoo < 50) { // countForFoo が50未満ならボタンのラベルを Observe に e.currentTarget.textContent = 'Observe'; }else{ // countForFoo が50以上ならボタンのラベルを Restart に e.currentTarget.textContent = 'Restart'; } }else{ //#foo の監視を開始 ro.observe(foo); //#bar の監視を開始 ro.observe(bar); //監視中かどうかのフラグを監視中に isObserved = true; // ボタンのラベルを Stop に e.currentTarget.textContent = 'Stop'; // countForFoo が50以上ならカウントをリセット if(countForFoo >= 50) { countForFoo = 0; countForBar = 0; } } });
この例の場合、変更回数が50に達すると、#bar の監視も停止するのでそれ以降の変更検知回数はカウントアップされません。
Stop をクリックすると監視を停止し、Observe をクリックすると監視を再開します。また、変更回数が50回に達するとボタンのラベルは Restart になり、カウントをリセットして監視を開始します。
コンストラクター ResizeObserver()
new 演算子を使用してコンストラクター ResizeObserver() にコールバック関数を渡すと、要素のサイズ変化を検知した際にコールバック関数を呼び出す ResizeObserver のインスタンスを作成して返します。
const observer = new ResizeObserver(callback)
コンストラクターの引数に、直接コールバック関数を記述することもできます。
const observer = new ResizeObserver((entries, observer) => { //コールバック関数の処理 });
引数(callback)
対象となる要素のサイズ変更が発生するたびに呼び出されるコールバック関数
戻り値(observer)
ResizeObserver のインスタンス(オブザーバー)。
コールバック関数
コンストラクター ResizeObserver() に渡すコールバック関数は以下の2つの引数を受け取ります。
引数 | 説明 |
---|---|
entries | ResizeObserverEntry オブジェクトの配列。ResizeObserverEntry オブジェクトはサイズ変更を検知した要素の変更後の新しいサイズやその要素の参照を持つオブジェクト。 |
observer | ResizeObserver 自身への参照。必要に応じてコールバック内からアクセスできます(特定の条件に達したときに unobserve() や disconnect() メソッドを使って監視を停止・解除する場合など)。必要ない場合は省略することができます。 |
const callback = (entries, observer) => { //要素のサイズ変更を検知した際に実行する処理 }
コールバックは一般に、以下のように for of
や forEach
などを使って ResizeObserverEntry オブジェクトの配列を反復処理します。
第二引数の observer は省略可能です。
const callback = (entries, observer) => { for (let entry of entries) { // 各 entry から情報を取得して処理 } } //コンストラクターにコールバック関数を渡してオブザーバーを生成 const observer = new ResizeObserver(callback);
const callback = (entries, observer) => { entries.forEach( (entry) => { // 各 entry から情報を取得して処理 }); } //コンストラクターにコールバック関数を渡してオブザーバーを生成 const observer = new ResizeObserver(callback);
コールバックのトリガー
オブザーバーは対象の要素のサイズ変更を検知する度にコールバック関数を呼び出します。以下のような場合も、サイズが変更されるのでコールバック関数が呼び出されます。
- 監視対象の要素にサイズが
0,0
でない要素が追加されたとき - 監視対象の要素からサイズが
0,0
でない要素が削除されたとき
また、以下のような場合なども変更として検知され、コールバック関数が呼び出されます。
- サイズが
0,0
でない監視対象の要素がレンダリングされるとき - 監視対象の要素の CSS の
display
プロパティがnone
になるかnone
でなくなったとき - 監視対象の要素が DOM から削除されたとき
以下は、監視対象の要素を <div id="target">Text</div>
として、1秒後にその要素に p
要素を追加し、2秒後にその p
要素を削除しています。コールバック関数では幅と高さを出力します。
const ro = new ResizeObserver(entries => { for (let entry of entries) { console.log('width: ' + entry.contentRect.width); console.log('height: ' + entry.contentRect.height); } }); const target = document.querySelector('#target'); ro.observe(target); console.log('開始'); setTimeout(() => { console.log('1秒後'); // p 要素を生成 const p = document.createElement('p'); // p 要素にテキストを設定 p.textContent = 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.'; // div#target に p 要素を追加 target.appendChild(p); }, 1000); setTimeout(() => { console.log('2秒後'); // div#target から p 要素を削除 target.removeChild(target.querySelector('p')) }, 2000);
監視対象の要素に要素が追加された際と、要素が削除された際にコールバック関数が実行されています。また、レンダリングの際もコールバック関数が実行されています。
例えば、以下のような3つの div 要素と button 要素があり、
<div id="foo">Foo</div> <div id="bar">Bar</div> <div id="baz">Baz</div> <button>Change</button>
#foo { width: 100px; } #bar { width: 200px; display: none; } #baz { width: 300px; }
以下の JavaScript を適用すると、
const ro = new ResizeObserver(entries => { for (let entry of entries) { console.log(entry.target.id + ' width: ' + entry.contentRect.width); } }); const foo = document.getElementById('foo'); const bar = document.getElementById('bar'); const baz = document.getElementById('baz'); ro.observe(foo); ro.observe(bar); ro.observe(baz); //ボタンのイベントリスナーを設定 document.querySelector('button').addEventListener('click', ()=> { console.log('button is clicked!'); //display プロパティを block から none に foo.style.display = 'none'; //display プロパティを none から block に bar.style.display = 'block'; //要素を削除 baz.parentNode.removeChild(baz); });
サイズが0
でない要素がレンダリングされる際にコールバック関数が実行され以下のように出力されます。#bar
は display:none
なので出力されません。
ボタンをクリックすると、変更が検知されて以下のように出力されます。
検知されない変更
但し、以下の場合は変更を検知できません(コールバック関数は呼び出されません)。
- 監視対象の要素の CSS の
display
プロパティがinline
の場合 - 監視対象の要素のサイズが
0,0
の場合 - CSS transforms による変更の場合
例えば、以下のように CSS を変更すると、
#foo { width: 100px; } #bar { width: 200px; /* display に inline を指定 */ display: inline; } #baz { /* サイズを 0 に */ width: 0; height: 0; }
display: inline
以外でサイズが 0 でない #foo
のみがレンダリングされる際にコールバック関数が実行され以下のように出力されます。
ボタンのリスナーを以下のように変更して、ボタンをクリックすると、
document.querySelector('button').addEventListener('click', ()=> { console.log('button is clicked!'); // transform の scale() で大きさを変更 foo.style.setProperty('transform', 'translateX(50px) scale(2,2)'); // インラインの要素に高さを設定(意味がない) bar.style.height = '100px'; // サイズを 0 から変更 baz.style.width = '100px'; baz.style.height = '20px'; });
display プロパティが inline の要素と CSS transforms のいずれの変更も検知されません。サイズが 0 からの変更は検知されます。
ResizeObserverEntry オブジェクト
ResizeObserverEntry オブジェクトはコールバック関数の第一引数に渡されるオブジェクトです。
以下のようなサイズ変更を検知した要素の参照や変更後の新しいサイズのプロパティを持ちます。
プロパティ | 説明 |
---|---|
target | サイズ変更が発生した監視対象の要素 |
contentRect | 監視中の要素の変更後の新しいサイズ(width や height など)を表すオブジェクト。※将来のバージョンで非推奨となる可能性があります。 |
contentBoxSize | 監視中の要素の新しいコンテンツボックスサイズを表すオブジェクトを含む配列。配列の各オブジェクトは blockSize と inlineSize プロパティを持ちます(現時点では配列に含まれるオブジェクトは1つ)。 |
borderBoxSize | 監視中の要素の新しい境界ボックスサイズを表すオブジェクトを含む配列。配列の各オブジェクトは blockSize と inlineSize プロパティを持ちます(現時点では配列に含まれるオブジェクトは1つ)。 |
devicePixelContentBoxSize | 監視中の要素の新しいデバイスピクセル単位のサイズを表すオブジェクトを含む配列。配列の各オブジェクトは blockSize と inlineSize プロパティを持ちます(現時点では配列に含まれるオブジェクトは1つ)。 |
※ コンテンツボックスはパディングとボーダーを含まず、境界ボックス(バウンディングボックス)はパディングとボーダーを含みます。
例えば、以下のような HTML と CSS の場合
<div class="targetDiv">Hello!</div>
.targetDiv { resize: both; overflow: auto; width: 200px; height: 50px; padding: 10px; margin: 20px; border: 1px solid #999; }
以下のような ResizeObserverEntry オブジェクトをコンソールに出力するコールバック関数で .targetDiv のサイズ変更を監視すると、
const ro = new ResizeObserver(entries => { for (let entry of entries) { //ResizeObserverEntry オブジェクトをコンソールに出力 console.dir(entry); } }); // .targetDiv の監視を開始 ro.observe(document.querySelector('.targetDiv'));
初期状態では以下のように出力されます。矢印で幅(width)に該当するプロパティを示していますが、contentRect
と contentBoxSize
は 200
、borderBoxSize
はパディングとボーダーを含む 222
、devicePixelContentBoxSize
は画面解像度により異なりますが以下では 400
となっています。
target
target プロパティには、サイズ変更が発生した監視対象の要素(Element または SVGElement )のオブジェクトがセットされます。
以下は前述の例のコールバック関数を書き換えて、サイズ変更が発生した監視対象の要素のプロパティなどを出力する例です。
const ro = new ResizeObserver(entries => { for (let entry of entries) { // タグ名を出力 console.log(entry.target.tagName); // DIV // クラス名を出力 console.log(entry.target.className); // targetDiv // テキストを出力 console.log(entry.target.textContent); // Hello! // CSS プロパティのオブジェクトを取得 const style = window.getComputedStyle(entry.target); // CSS の border の値を出力 console.log(style.border); // 1px solid rgb(153, 153, 153) } }); ro.observe(document.querySelector('.targetDiv'));
contentRect
contentRect プロパティには、監視中の要素の変更後の新しいサイズを表すオブジェクトがセットされます(要素が HTML Element の場合は要素のコンテンツボックス、要素が SVGElement の場合は SVG の境界ボックス)。
以下は前述の例のコールバック関数を書き換えて、サイズ変更が発生した監視対象の要素の width
と height
を出力する例です。幅と高さ以外にも座標などの情報も取得できます。
const ro = new ResizeObserver(entries => { for (let entry of entries) { // contentRect から幅を取得 const width = entry.contentRect.width; // contentRect から高さを取得 const height = entry.contentRect.height; console.log(width); //200(初期値の例) console.log(height); //50(初期値の例) } }); ro.observe(document.querySelector('.targetDiv'));
将来のバージョンで非推奨となる可能性
contentRect
プロパティは現時点では最も多くのブラウザでサポートされていますが、 ResizeObserver API の以前の(古い)実装から残ったもので、将来のバージョンで非推奨となる可能性があります。
以下は Editor’s Draft からの抜粋です。
contentRect is from the incubation phase of ResizeObserver and is only included for current web compat reasons. It may be deprecated in future levels.
現時点ではほとんどのブラウザで、contentBoxSize
プロパティもサポートされています。
例えば、以下のように記述すれば、contentBoxSize
がブラウザでサポートされていない場合は、contentRect
からサイズを取得することができます。
const width = entry.contentBoxSize ? entry.contentBoxSize[0].inlineSize : entry.contentRect.width; const height = entry.contentBoxSize ? entry.contentBoxSize[0].blockSize : entry.contentRect.height;
関連項目:古いブラウザへの対応
contentBoxSize
contentBoxSize プロパティには、監視中の要素の新しいコンテンツボックスサイズを表すオブジェクトを含む配列
がセットされます。
現時点では配列にはコンテンツボックスサイズを表す1つのオブジェクトしか含まれておらず、以下のプロパティを持っています。
プロパティ | 説明 |
---|---|
blockSize | 監視された要素のコンテンツボックスのブロック方向の長さ。一般的な writing-mode が横書きの場合、高さになります(height に相当)。 |
inlineSize | 監視された要素のコンテンツボックスのインライン方向の長さ。writing-mode が横書きの場合、幅になります(width に相当)。 |
以下は前述の例のコールバック関数を書き換えて、サイズ変更が発生した監視対象の要素の幅(inlineSize
)と高さ(blockSize
)を出力する例です。
配列には1つのオブジェクトしか含まれていないので、サイズを表すオブジェクトには contentBoxSize[0]
でアクセスできます。
const ro = new ResizeObserver(entries => { for (let entry of entries) { // contentBoxSize[0] から幅を取得 const width = entry.contentBoxSize[0].inlineSize; // contentBoxSize[0] から高さを取得 const height = entry.contentBoxSize[0].blockSize; console.log(width); //200(初期値の例) console.log(height); //50(初期値の例) } }); ro.observe(document.querySelector('.targetDiv'));
borderBoxSize
borderBoxSize プロパティには、監視中の要素の新しい境界ボックスサイズ(パディングとボーダーを含むサイズ)を表すオブジェクトを含む配列
がセットされます。
contentBoxSize プロパティ同様、現時点では配列には境界ボックスサイズを表す1つのオブジェクトしか含まれておらず、blockSize
(高さ)と inlineSize
(幅)プロパティを持ちます。
以下は前述の例のコールバック関数を書き換えて、サイズ変更が発生した監視対象の要素の幅(inlineSize
)と高さ(blockSize
)を出力する例です。
配列には1つのオブジェクトしか含まれていないので、サイズを表すオブジェクトには borderBoxSize[0]
でアクセスできます。
この例の場合、パディング(10px)とボーダー(1px)が指定されているので、コンテンツのサイズに 22px 加算した値になります。
const ro = new ResizeObserver(entries => { for (let entry of entries) { // borderBoxSize[0] から幅を取得 const width = entry.borderBoxSize[0].inlineSize; // borderBoxSize[0] から高さを取得 const height = entry.borderBoxSize[0].blockSize; console.log(width); //222(初期値の例)パディングとボーダーを含む console.log(height); //72(初期値の例)パディングとボーダーを含む } }); ro.observe(document.querySelector('.targetDiv'));
borderBoxSize プロパティで取得できる値は、要素の offsetWidth
、offsetHeight
プロパティや getBoundingClientRect() メソッドから得られる width
、height
プロパティの値と同じです。
関連項目:要素のサイズと位置
const targetDiv = document.querySelector('.targetDiv'); console.log(targetDiv.offsetWidth); //222 console.log(targetDiv.offsetHeight); //72 console.log(targetDiv.getBoundingClientRect().width); //222 console.log(targetDiv.getBoundingClientRect().height); //72
devicePixelContentBoxSize
devicePixelContentBoxSize プロパティには、監視中の要素の新しいデバイスピクセル単位のサイズを表すオブジェクトを含む配列
がセットされます。
contentBoxSize や borderBoxSize プロパティ同様、現時点では配列には境界ボックスサイズを表す1つのオブジェクトしか含まれておらず、blockSize
(高さ)と inlineSize
(幅)プロパティを持ちます。
以下は前述の例のコールバック関数を書き換えて、サイズ変更が発生した監視対象の要素の幅(inlineSize
)と高さ(blockSize
)を出力する例です。
配列には1つのオブジェクトしか含まれていないので、サイズを表すオブジェクトには devicePixelContentBoxSize[0]
でアクセスできます。
const ro = new ResizeObserver(entries => { for (let entry of entries) { // devicePixelContentBoxSize[0] から幅を取得 const width = entry.devicePixelContentBoxSize[0].inlineSize; // devicePixelContentBoxSize[0] から高さを取得 const height = entry.devicePixelContentBoxSize[0].blockSize; console.log(width); //400(初期値の例 ※画面解像度により異なる) console.log(height); //100(初期値の例 ※画面解像度により異なる) } }); ro.observe(document.querySelector('.targetDiv'));
- can i use devicePixelContentBoxSize(現時点では Safari/iOS ではサポートされていません)
ResizeObserver インスタンス
ResizeObserver() コンストラクターで生成された ResizeObserver のインスタンス(オブザーバー)では以下のメソッドを利用できます。
メソッド | 説明 |
---|---|
observe() | 指定された要素の監視を開始します |
unobserve() | 指定された要素の監視を終了します |
disconnect() | 全ての対象要素の監視を解除します |
プロパティはありません。
observe()
ResizeObserver インターフェイスのメソッドで、指定された要素(Element または SVGElement)の監視を開始し、サイズ変更を検知するとコールバック関数を呼び出します。
unobserve() または disconnect() メソッドで停止するまで監視は続きます。停止後、再度 observe() メソッドを呼び出してオブザーバーを再利用することができます。
resizeObserver.observe(target, options)
第二引数(options)には監視のオプションを指定することができます(省略可能)。現在、設定可能なオプションは1つだけです。
オプション | 説明 |
---|---|
box | オブザーバーがどのボックスモデルの変更を観察するかを設定。設定可能な値は以下。
|
第二引数(options)にプションオブジェクトを使った observe()
の呼び出しの例。
※ どれを指定しても変わらない?
resizeObserver.observe(divElem, { box : 'border-box' });
検知のタイミング
ResizeObserver でのサイズ変更の検知は、window.resize イベントの発生とほぼ同じ頻度になるようです。また、MDN の ResizeObserver には以下のように記載されていて、ResizeObserver の実行は描画サイクルに直接結び付けられているようです。
仕様に従っている場合、実装は描画の前およびレイアウトの後にリサイズイベントを呼び出します。
また、以下は web.dev のレポートされるタイミングは? からの抜粋です。
仕様では、ResizeObserver がペイント前とレイアウト後にすべてのサイズ変更イベントを処理することを禁じています。そのため、ResizeObserver のコールバックが、ページのレイアウトを変更するのに理想的な場所となります。
以下は、ResizeObserver と window.resize イベントで幅の変更を検知してコンソールに出力する例です。
const ro = new ResizeObserver( (entries) => { for (let entry of entries) { console.log('ResizeObserver: ' + entry.contentRect.width); } }); ro.observe(document.body); window.addEventListener('resize', () => { console.log('Window Resize: ' + window.innerWidth) });
ブラウザの幅を変更すると、例えば以下のように出力され、ResizeObserver と window.resize イベントはほぼ同じタイミングで発生しているようです。
また、以下は ResizeObserver のサイズ変更検知の発生間隔を出力する例です(この方法で正しく計測できるかは定かではありませんが)。
let prevTime = performance.now(); const ro = new ResizeObserver( (entries) => { for (let entry of entries) { const now = performance.now(); //経過時間 const elapsed = Math.round((now - prevTime) *10)/10; console.log('width:' + entry.contentRect.width); console.log('Elapsed: ' + elapsed + 'ms'); prevTime = now; } }); ro.observe(document.body);
ブラウザの幅を変更すると、例えば以下のように出力されます。出力される値は環境や状況で異なります(例えば、開発ツールのコンソールタブを開いていると経過時間は長くなるようです)。
最初の経過時間 Elapsed: 0.6ms
は初回のレンダリング時のもので、次の Elapsed: 11064.1ms
は幅の変更を開始するまでの経過時間です。
ちなみにフレームレートが 60 FPS
の場合、requestAnimationFrame() でのコールバック関数の呼び出しは 1000ms ÷ 60 = 16.6666…
なので約16.7ms
に1回になります。
Debounce の利用
例えば、ユーザーがサイズを変更した後に何らかの処理を行えば良い場合、つまりリサイズ中には処理が不要な場合はイベント終了時に処理を実行すれば良いので、変更が発生する度にコールバック関数を実行する必要はないわけです。
以下は setTimeout() と clearTimeout() を使ってサイズ変更が終了した時点から一定の時間経過した際にコールバック関数を実行する例です。
<div class="debounce"></div> <p>width: <span class="print-width"></span></p>
.debounce { resize: both; overflow: auto; width: 200px; height: 50px; border: 1px solid #ccc; }
この例ではサイズ変更が終了して200ミリ秒経過すると、乱数を使った背景色を対象の要素に設定し、その要素の幅を出力します。
仕組みとしては、変数 delay
で指定した時間に満たないうちに変更を検知すると、clearTimeout(timer)
により setTimeout()
に指定した処理はキャンセルされ、delay
で指定した時間を経過すると setTimeout()
に指定した処理が実行されます。
let timer = 0; //終了と判定するまでの遅延時間(ミリ秒) const delay = 200; const ro = new ResizeObserver((entries) => { clearTimeout(timer); timer = setTimeout( () => { for (let entry of entries) { //乱数で16進数の色を生成 const color = "#" + Math.random().toString(16).slice(-6); //背景色を設定 entry.target.style.backgroundColor = color; //幅を span 要素に出力 document.querySelector('.print-width').textContent = entry.contentRect.width; } }, delay); }); //監視を開始 ro.observe(document.querySelector('.debounce'));
関連項目:イベント終了時に処理を実行
上記のような連続して繰り返される処理が指定した時間内に発生した場合に、最後の1回だけ実行する処理を Debounce と呼びます。
以下のような Debounce 用の独自の関数 debounce()
を定義して利用することもできます。
debounce()
の第一引数にコールバック関数を、第二引数に終了と判定するまでの遅延時間を指定します。
//Debounce 用の関数の定義 const debounce = (func, delay) => { let timer = 0; return (...args) => { clearTimeout(timer); timer = setTimeout(() => func.apply(null, args), delay); } } // debounce() を使ったコールバック関数をコンストラクターに渡す const ro = new ResizeObserver( debounce( (entries) => { for (let entry of entries) { //乱数で16進数の色を生成 const color = "#" + Math.random().toString(16).slice(-6); //背景色を設定 entry.target.style.backgroundColor = color; //幅を span 要素に出力 document.querySelector('.print-width').textContent = entry.contentRect.width; } }, 200)); //監視を開始 ro.observe(document.querySelector('.debounce'));
以下の div 要素の右下端をドラッグしてサイズを変更すると、サイズ変更が完了してから 200ms 後に背景色が変わり、幅の値が更新されます。
width:
関連ページ
Throttle の利用
Debounce とは異なりますが、似たような処理に Throttle があります。Throttle は連続して繰り返される処理を一定間隔で間引くものです。
以下はサイズ変更が発生した際に、delay
で指定した時間に1回コールバック関数を実行する例です。
delay で指定した時間が経過すると、timer
に 0
を設定して setTimeout()
で指定した処理が実行されますが、delay
で指定した時間未満の場合は if(timer) return
により何も実行されません。
let timer; const delay = 500; const ro = new ResizeObserver((entries) => { // timer が正の整数(0以外)であれば何もしない if ( timer ) return ; //setTimeout() は正の整数値を返す timer = setTimeout( () => { // timer に 0 を設定 timer = 0 ; for (let entry of entries) { //乱数で16進数の色を生成 const color = "#" + Math.random().toString(16).slice(-6); //背景色を設定 entry.target.style.backgroundColor = color; //幅を span 要素に出力 document.querySelector('.print-width').textContent = entry.contentRect.width; } }, delay); }); ro.observe(document.querySelector('.debounce'));
関連項目:イベントの処理回数を減らす
但し、上記の場合、delay で指定した遅延時間内に発生した最後のサイズ変更は無視されてしまいます。
以下は最後に発生したサイズ変更に対してコールバック関数を実行するようにした Throttle 用の独自の関数 throttle()
の例です。
//Throttle 用の関数の定義 const throttle = (func, limit) => { let lastFunc let lastRan return (...args) => { if (!lastRan) { func.apply(null, args) lastRan = Date.now() } else { clearTimeout(lastFunc) lastFunc = setTimeout(() => { if (Date.now() - lastRan >= limit) { func.apply(null, args) lastRan = Date.now() } }, limit - (Date.now() - lastRan)) } } } // throttle() を使ったコールバック関数を渡す const ro = new ResizeObserver(throttle((entries) => { for (let entry of entries) { //乱数で16進数の色を生成 const color = "#" + Math.random().toString(16).slice(-6); //背景色を設定 entry.target.style.backgroundColor = color; //幅を span 要素に出力 document.querySelector('.print-width').textContent = entry.contentRect.width; } }, 500)); ro.observe(document.querySelector('.debounce'));
以下の div 要素の右下端をドラッグしてサイズを変更すると、500ms に一回コールバックが実行されて背景色が変わり、幅の値が更新されます。
width:
ResizeObserver のサンプル
サイズによりクラスを設定
要素の幅と高さによりクラスを設定(追加・削除)するサンプルです。
以下のサンプルでは、target
クラスを指定した要素のサイズを監視して、幅の値により、small
、medium
、large
というクラスを、縦横比の値により、portrait
、landscape
、square
というクラスを設定します。
modes
は変更後の幅の値からクラス名を生成するためのオブジェクトの配列で、各オブジェクトは幅の最小値(min
)と最大値(max
)、及びクラス名(name
)のプロパティを持っています。
例えば、変更後の幅が 500px の場合は、min: 300
と max: 600
を持つオブジェクトに該当するので、medium
というクラスを設定します。
同様に、ratios
は変更後の縦横比からクラス名を生成するためのオブジェクトの配列です。
DOMContentLoaded イベントを使って、DOM ツリーの解析が完了した時点で、target
クラスの要素が存在すれば ResizeObserver のインスタンスを生成して、サイズ変更の監視を開始します。
この例では、サイズは borderBoxSize を使って、ボーダーとパディングを含む値にしています。
// 要素の幅を基にクラスを設定するためのオブジェクトの配列 const modes = [ { name: "small", min: 0, max: 300 }, { name: "medium", min: 300, max: 600 }, { name: "large", min: 600, max: Infinity }, ]; // クラス名の配列 const modeClasses = []; modes.forEach((mode) => { // modes の各オブジェクトの name の値を modeClasses に追加 modeClasses.push(mode.name); }); // 要素の縦横比を基にクラスを設定するためのオブジェクトの配列 const ratios = [ { name: "portrait", min: 0, max: 0.875 }, { name: "square", min: 0.875, max: 1.125 }, { name: "landscape", min: 1.125, max: Infinity }, ]; // クラス名の配列 const ratioClasses = []; ratios.forEach((ratio) => { // ratios の各オブジェクトの name の値を ratioClasses に追加 ratioClasses.push(ratio.name); }); document.addEventListener("DOMContentLoaded", () => { //target クラスの要素を取得 const targets = document.querySelectorAll('.target'); //target クラスの要素が存在すれば if(targets.length > 0) { // ResizeObserver のインスタンスを生成 const resizeObserver = new ResizeObserver((entries) => { for (entry of entries) { //サイズが変更された監視対象の要素 const target = entry.target; //その要素の変更後の幅(パディングとボーダーを含む) const width = entry.borderBoxSize[0].inlineSize; //その要素の変更後の高さ(パディングとボーダーを含む) const height = entry.borderBoxSize[0].blockSize; //縦横比 const ratioValue = width / height; //その要素に指定されているクラス const targetClassList = target.classList; //配列 modes から変更後の幅にマッチするするオブジェクトを取得 const mode = modes.find((mode) => mode.min <= width && mode.max >= width); modeClasses.forEach((value) => { //mode のクラス(small, medium, large)が指定されていれば一度クリア targetClassList.remove(value); }); ///mode のクラスを追加 targetClassList.add(mode.name); //配列 ratios から変更後の縦横比にマッチするするオブジェクトを取得 const ratio = ratios.find((ratio) => ratio.min <= ratioValue && ratio.max >= ratioValue); ratioClasses.forEach((value) => { //ratio のクラス(portrait, square, landscape)が指定されていれば一度クリア targetClassList.remove(value); }); //ratio のクラスを追加 targetClassList.add(ratio.name); } }); //それぞれの監視対象の要素 targets.forEach((target) => { //target クラスの要素の監視を開始 resizeObserver.observe(target); }); } }, false);
以下のように modes にオブジェクトを追加すれば、設定するクラスを増やすことができます。
const modes = [ { name: "xs", //追加 min: 0, max: 100 }, { name: "small", min: 100, max: 300 }, { name: "medium", min: 300, max: 600 }, { name: "large", min: 600, max: 900 }, { name: "xl", //追加 min: 900, max: Infinity } ];
以下のサンプルでは div 要素のスタイルに resize: both と overflow: auto を指定しているので、右下端をドラッグしてサイズを変更できます。
この例の場合、どれも実用的なものではありませんが、幅を変更すると背景色とフォントサイズが変わり、縦横比が変わるとボーダーのスタイルが変わるスタイルを設定してあります。
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Cumque voluptas distinctio accusamus doloremque minus sapiente perferendis ut quisquam perspiciatis omnis deleniti praesentium deserunt rerum laudantium corrupti rem fugit, eum quo.
Deserunt saepe minima eligendi iusto reiciendis magni, omnis ab in officiis impedit? Nemo nobis perferendis iste ex veritatis omnis saepe earum odio. Itaque qui alias possimus? Repellat voluptate aliquid quia!
Corporis tempora reiciendis a ullam nobis provident est aspernatur consequuntur similique vel. Consequatur deleniti omnis qui consectetur at, porro dolore amet labore quas molestiae voluptates, animi error iste, odit a.
参考サイト:How to Build a Highly Responsive UI with ResizeObserver API
Debounce や Throttle の利用
ResizeObserver でのサイズ変更は window.resize イベントの発生と同様、高い頻度で検知されるので、必要に応じて Debounce や Throttle の利用を検討すると良いと思います。
<div class="target resizable"> <p>Corporis tempora reiciendis a ullam nobis provident est aspernatur consequuntur similique vel. Consequatur deleniti omnis qui consectetur at, porro dolore amet labore quas molestiae voluptates, animi error iste, odit a.</p> <p>Deserunt saepe minima eligendi iusto reiciendis magni, omnis ab in officiis impedit? Nemo nobis perferendis iste ex veritatis omnis saepe earum odio. Itaque qui alias possimus? Repellat voluptate aliquid quia!</p> <p>Corporis tempora reiciendis a ullam nobis provident est aspernatur consequuntur similique vel. Consequatur deleniti omnis qui consectetur at, porro dolore amet labore quas molestiae voluptates, animi error iste, odit a.</p> </div>
* { box-sizing: border-box; } .target { background-color: #eee; min-width: 100px; width: 90%; padding: 1rem; margin: 20px; } .resizable { resize: both; overflow: auto; } .small { background-color: #f7d5f0; font-size: 14px; } .medium { background-color: #dcf2f9; font-size: 16px; } .large { background-color: #d7f7d7; font-size: 18px; } .landscape { border: 1px solid #666; } .portrait { border: 3px double #666; } .square{ border: 5px dotted #666; }
チャットウィンドウ
チャットウィンドウではウィンドウを会話(チャットテキスト)の下部に固定し、最新のメッセージが表示されるようにするのが一般的です。
また、レイアウト変更しても最新のメッセージを表示するように下部に固定しますが、ResizeObserver を使うと簡単に実現することができます。
以下がチャットウィンドウの HTML です。.chat の要素にスクロールバーを表示するようにスタイルを設定します。
<!-- スクロールバーを表示する要素 --> <div class="chat"> <!-- チャットのテキストが追加される要素 --> <div class="chat-text"> <!-- チャットのテキスト --> <div><strong>Foo</strong> : hi </div> <div><strong>Bar</strong> : hi </div> </div> </div> <!-- ダミーのチャットテキストを追加するボタン --> <button id="add-chat">Add Chat Message</button>
外側の要素にスクロールバーを表示するように overflow: scroll
と height
を指定します。この例ではサイズを変更できるように resize: both
も指定しています。
/* スクロールバーを表示する要素 */ .chat { overflow: scroll; /* スクロールバーを表示 */ height: 440px; /* 高さを指定 */ width: 440px; min-width: 200px; border: 1px solid #999; padding: .5rem; resize: both; margin-top: 1rem; background-color: #ccc; } /* チャットテキストの要素 */ .chat-text > div { background-color: #fff; margin: 1rem 0; padding: 5px; } /* ダミーのチャットテキストを追加するボタン */ #add-chat { background-color: #607b69; color: #fff; padding: .5rem 1rem; border: 1px solid #31573e; cursor: pointer; }
チャットテキストが追加される要素(.chat-text
)を監視対象にすることで、チャットテキスト(子要素)が追加されるとサイズの変更が検知されます。
コールバック関数では、entry.target.parentNode
で親要素(.chat
)を取得し、垂直にスクロールする量を設定するだけです。
scrollTop は垂直方向にスクロールする量(ピクセル数)を設定(または取得)するプロパティです。
scrollHeight(画面上に表示されない部分を含めた要素の中身の高さ)から clientHeight(要素の内部の高さ)を差し引いた分だけスクロールさせれば、下部に固定することができます。
ボタンをクリックすると、ダミーのチャットテキストが追加されます。また、右下のハンドルをドラッグしてサイズを変更することができます。