「この要素は今どの位置にあるのか」「実際の幅は何ピクセルか」を JavaScript から知りたい場面は意外と多くあります。スクロールに合わせて何かを表示したり、クリックした場所を要素内の座標に変換したり、ツールチップを要素の真下に出したり。そうしたときに使うのが getBoundingClientRect() です。この記事では、このメソッドで取得できる値の意味、座標の基準がどこなのか、offsetWidth との違い、そして実際の使いどころと注意点までを解説します。
目次
getBoundingClientRect() で取得できる値
getBoundingClientRect() は要素に対して呼び出すメソッドで、その要素を囲む長方形の情報を持った DOMRect オブジェクトを返します。引数はありません。返ってくるオブジェクトには次の8つのプロパティが入っています。
| プロパティ | 意味 |
|---|---|
top | 要素の上端の Y 座標 |
left | 要素の左端の X 座標 |
right | 要素の右端の X 座標。left + width と同じ値 |
bottom | 要素の下端の Y 座標。top + height と同じ値 |
width | 要素の幅。padding と border を含む |
height | 要素の高さ。padding と border を含む |
x | left と同じ値 |
y | top と同じ値 |
x と y がそれぞれ left・top と同じ値になるのは、getBoundingClientRect() が返す長方形の width・height が負にならないためです。DOMRect という型自体は幅や高さに負の数を持てる仕様で、その場合に x と left がずれますが、このメソッドの戻り値で気にする必要はありません。どちらを使っても構いませんが、top / left のほうが意味が読み取りやすいでしょう。
実際に呼び出すと、次のように値を取り出せます。
const box = document.querySelector('.box');
const rect = box.getBoundingClientRect();
console.log(rect.top); // => 120.5(ビューポート上端からの距離)
console.log(rect.left); // => 32
console.log(rect.width); // => 300.75
console.log(rect.height); // => 180
// 分割代入でまとめて取り出してもよい
const { top, left, width, height } = box.getBoundingClientRect();
値は整数とは限らず、120.5 のように小数を含む点も押さえておきましょう。パーセント指定や flex による分配、拡大縮小の結果として、実際のレイアウトは小数のピクセル値になることがあるためです。
また、返ってきた rect は呼び出した瞬間のスナップショットです。スクロールしたりウィンドウサイズが変わったりしても、変数に入れておいた rect の中身は自動では更新されません。最新の値が必要なタイミングで呼び直す必要があります。
座標の基準はビューポートの左上
このメソッドで最も間違えやすいのが座標の基準です。top や left はビューポート(ブラウザーの表示領域)の左上を原点 (0, 0) とした値であり、ページ全体の先頭からの距離ではありません。
そのため、ページをスクロールすると同じ要素でも top の値が変わります。画面の上に流れていった要素は top がマイナスになり、まだ画面より下にある要素は window.innerHeight(表示領域の高さ)より大きい値になります。次のデモは、対象の要素の値をリアルタイムに表示するものです。プレビューの中をスクロールして、値がどう変化するか確かめてみてください。要素が画面内に入ると色が変わります。
<p class="hint">下にスクロールしてみてください</p>
<div class="spacer"></div>
<div class="box" id="box">対象の要素</div>
<div class="spacer"></div>
<div class="panel" id="panel"></div>
body {
margin: 0;
padding: 16px 16px 96px;
font-family: sans-serif;
}
.hint {
margin: 0 0 8px;
font-size: 13px;
color: #666;
}
.spacer {
height: 260px;
}
.box {
display: flex;
align-items: center;
justify-content: center;
height: 120px;
border: 4px solid #007bff;
border-radius: 8px;
background: #e7f1ff;
font-weight: bold;
color: #0b4a8f;
}
/* 画面内に入ったら色を変える */
.box.is-visible {
border-color: #28a745;
background: #e6f6ea;
color: #1b6b30;
}
.panel {
position: fixed;
right: 12px;
bottom: 12px;
left: 12px;
padding: 10px 12px;
border-radius: 6px;
background: rgba(33, 37, 41, 0.9);
color: #fff;
font-family: monospace;
font-size: 13px;
line-height: 1.7;
white-space: pre;
}
const box = document.getElementById('box');
const panel = document.getElementById('panel');
function update() {
// 要素の位置とサイズを取得する(基準はビューポートの左上)
const rect = box.getBoundingClientRect();
panel.textContent =
'top: ' + rect.top.toFixed(1) + ' bottom: ' + rect.bottom.toFixed(1) + '\n' +
'width: ' + rect.width.toFixed(1) + ' height: ' + rect.height.toFixed(1) + '\n' +
'ページ先頭からの位置: ' + (rect.top + window.scrollY).toFixed(1);
// 画面内に入っているかの判定
const inView = rect.top < window.innerHeight && rect.bottom > 0;
box.classList.toggle('is-visible', inView);
}
update();
window.addEventListener('scroll', update);
window.addEventListener('resize', update);
スクロール量に関係のない「ページ先頭からの位置」がほしい場合は、現在のスクロール量を足します。縦方向なら window.scrollY、横方向なら window.scrollX です。
const rect = target.getBoundingClientRect();
// ビューポート基準(スクロールすると変わる)
console.log(rect.top);
// ドキュメント(ページ先頭)基準(スクロールしても変わらない)
const pageTop = rect.top + window.scrollY;
const pageLeft = rect.left + window.scrollX;
// この位置までスクロールさせる、といった使い方ができる
window.scrollTo({ top: pageTop - 20, behavior: 'smooth' });
この使い分けはそのまま CSS の position の使い分けにも対応します。position: fixed で配置する要素はビューポート基準なので rect の値をそのまま使えます。一方 position: absolute で(配置の基準となる祖先要素がなく)ページ全体を基準に置く場合は、スクロール量を足した値が必要になります。
offsetWidth・clientWidth との違い
要素のサイズを測る方法としては offsetWidth と clientWidth もあります。どれも「幅」を返しますが、含まれる範囲と精度が違うため、目的に応じて選ぶ必要があります。
getBoundingClientRect().width | offsetWidth | clientWidth | |
|---|---|---|---|
| 含まれる範囲 | padding と border を含む | padding と border を含む | padding のみ(border は含まない) |
| スクロールバー | 含む | 含む | 含まない |
| 戻り値 | 小数を含む数値 | 整数に丸めた数値 | 整数に丸めた数値 |
| transform の影響 | 受ける(見た目のサイズ) | 受けない(レイアウト上のサイズ) | 受けない(レイアウト上のサイズ) |
いちばん実害が出やすいのが transform の扱いです。transform: scale(2) を当てた要素は見た目が2倍になりますが、レイアウト上の大きさは変わっていません。そのため offsetWidth は元のままの値を返し、getBoundingClientRect().width は2倍の値を返します。
// .card { width: 200px; transform: scale(2); } の場合
const card = document.querySelector('.card');
console.log(card.offsetWidth); // => 200(レイアウト上の幅)
console.log(card.getBoundingClientRect().width); // => 400(画面に見えている幅)
画面上での見た目の位置やサイズを扱いたい(当たり判定、重なりの計算、要素に合わせた配置など)なら getBoundingClientRect()、CSS で指定したレイアウト上のサイズを知りたいなら offsetWidth、という切り分けが基本になります。小数まで正確に測りたいときも getBoundingClientRect() です。整数に丸められた offsetWidth を積み上げて計算すると、要素数が多いときに1〜2ピクセルのずれとして表面化することがあります。
なお、要素の内側にスクロールバーが出ているとき、その分の幅を含めたくないなら clientWidth を使います。中身を配置できる領域の幅を知りたい場面ではこちらが適しています。
要素が画面内に入っているか判定する
座標がビューポート基準であることを利用すると、要素が今画面に見えているかどうかを簡単に判定できます。「一部でも見えているか」は、要素の上端が画面の下端より上にあり、かつ下端が画面の上端より下にある、と言い換えられます。
// 要素が一部でも画面内に入っているか
function isInViewport(el) {
const rect = el.getBoundingClientRect();
const viewHeight = document.documentElement.clientHeight;
const viewWidth = document.documentElement.clientWidth;
return (
rect.top < viewHeight &&
rect.bottom > 0 &&
rect.left < viewWidth &&
rect.right > 0
);
}
// 要素の全体が画面内に収まっているか
function isFullyVisible(el) {
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= document.documentElement.clientHeight &&
rect.right <= document.documentElement.clientWidth
);
}
ビューポートの高さには window.innerHeight を使うこともできます。違いはスクロールバーの幅を含むかどうかで、window.innerHeight / window.innerWidth はスクロールバーを含んだ値、document.documentElement.clientHeight / clientWidth はスクロールバーを除いた値です。横方向の判定を厳密に行いたい場合は後者のほうが実際の表示領域に一致します。
クリックした位置を要素内の座標に変換する
マウスイベントの event.clientX / event.clientY も、getBoundingClientRect() と同じくビューポートの左上が基準です。基準がそろっているので、単純な引き算だけで「要素の左上から見てどこをクリックしたか」が求められます。
stage.addEventListener('click', (e) => {
const rect = stage.getBoundingClientRect();
const x = e.clientX - rect.left; // 要素の左端からの距離
const y = e.clientY - rect.top; // 要素の上端からの距離
console.log(x, y);
});
この計算は、キャンバスへの描画、ドラッグ操作、カラーピッカーやスライダーのような自作 UI、画像の拡大鏡など、要素内の相対位置が必要になるあらゆる場面で使います。次のデモでは、クリックした位置に印を付けつつ、要素内の座標と全体に対する割合を表示しています。
<div class="stage" id="stage">
<span class="stage__label">この中をクリックしてください</span>
<span class="marker" id="marker"></span>
<span class="tooltip" id="tooltip"></span>
</div>
<p class="result" id="result">クリックした位置がここに表示されます</p>
body {
margin: 0;
padding: 16px;
font-family: sans-serif;
}
.stage {
position: relative; /* 中の要素を絶対配置する基準にする */
height: 220px;
border: 2px dashed #007bff;
border-radius: 8px;
background: #f4f8ff;
cursor: crosshair;
overflow: hidden;
}
.stage__label {
position: absolute;
top: 8px;
left: 12px;
color: #6c8bb5;
font-size: 13px;
}
.marker {
position: absolute;
width: 12px;
height: 12px;
margin: -6px 0 0 -6px; /* 中心をクリック位置に合わせる */
border-radius: 50%;
background: #dc3545;
opacity: 0;
}
.tooltip {
position: absolute;
padding: 4px 8px;
border-radius: 4px;
background: #212529;
color: #fff;
font-size: 12px;
white-space: nowrap;
transform: translate(-50%, -140%);
opacity: 0;
}
.marker.is-shown,
.tooltip.is-shown {
opacity: 1;
}
.result {
margin: 12px 0 0;
font-family: monospace;
font-size: 13px;
color: #333;
}
const stage = document.getElementById('stage');
const marker = document.getElementById('marker');
const tooltip = document.getElementById('tooltip');
const result = document.getElementById('result');
stage.addEventListener('click', (e) => {
const rect = stage.getBoundingClientRect();
// clientX / clientY もビューポート基準なので、そのまま引き算できる
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
marker.style.left = x + 'px';
marker.style.top = y + 'px';
marker.classList.add('is-shown');
tooltip.style.left = x + 'px';
tooltip.style.top = y + 'px';
tooltip.textContent = 'x: ' + Math.round(x) + ' / y: ' + Math.round(y);
tooltip.classList.add('is-shown');
// 要素の左端・上端からの割合も出せる
const ratioX = ((x / rect.width) * 100).toFixed(1);
const ratioY = ((y / rect.height) * 100).toFixed(1);
result.textContent =
'要素内の座標 → x: ' + Math.round(x) + 'px, y: ' + Math.round(y) + 'px' +
'(横 ' + ratioX + '% / 縦 ' + ratioY + '%)';
});
rect.width で割れば 0 〜 1 の割合が得られるので、要素のサイズが変わっても同じ意味を保てます。スライダーの値を求めるときなどは、ピクセル値ではなく割合で扱うほうが扱いやすいでしょう。
要素に合わせてツールチップを配置する
同じ考え方で、ある要素の真下や真横に別の要素を出すこともできます。ボタンの rect を取得し、その bottom(下端)と left + width / 2(水平方向の中央)を使って位置を決めます。
const button = document.querySelector('.tooltip-trigger');
const tooltip = document.querySelector('.tooltip');
button.addEventListener('mouseenter', () => {
const rect = button.getBoundingClientRect();
// tooltip は position: fixed。ビューポート基準なのでそのまま使える
tooltip.style.top = `${rect.bottom + 8}px`;
tooltip.style.left = `${rect.left + rect.width / 2}px`;
tooltip.hidden = false;
});
button.addEventListener('mouseleave', () => {
tooltip.hidden = true;
});
ツールチップ側には transform: translateX(-50%) を当てておきます。こうすると指定した X 座標がツールチップの中央になり、ボタンの真下に来ます。position: absolute でページ全体を基準に置く場合は、前述のとおり rect.bottom + window.scrollY のようにスクロール量を足してください。
値がすべて 0 になるときに確認すること
「取得できているはずなのに top も width も 0 が返ってくる」という相談はよくあります。エラーにならず 0 が返るだけなので、原因に気付きにくいのが厄介なところです。ほとんどの場合、原因は次のどちらかです。
要素が display: none になっている
display: none の要素はレイアウトの計算対象から外れ、そもそも大きさや位置を持ちません。そのため getBoundingClientRect() はすべて 0 の長方形を返します。モーダルやアコーディオンのように、開いたときに初めてサイズを測りたいケースでよく踏みます。
対処は単純で、表示してから測ることです。要素を表示状態にしてから getBoundingClientRect() を呼べば正しい値が返ります。
const panel = document.querySelector('.panel'); // display: none の状態
console.log(panel.getBoundingClientRect().height); // => 0
// 表示してから測る
panel.style.display = 'block';
console.log(panel.getBoundingClientRect().height); // => 240 など実際の高さ
アニメーションのために「隠したまま高さだけ知りたい」という場合は、display: none ではなく visibility: hidden や opacity: 0 を使う方法があります。これらは領域を占有したままなので、サイズも位置も正しく取得できます。
まだ DOM に追加されていない
createElement() で作っただけの要素も、すべて 0 を返します。ドキュメントに挿入されていない要素はレンダリングツリーに含まれず、位置も大きさも決まらないためです。
const el = document.createElement('div');
el.textContent = 'テキスト';
// まだどこにも追加していないので 0
console.log(el.getBoundingClientRect().width); // => 0
// 追加してから測ればよい
document.body.appendChild(el);
console.log(el.getBoundingClientRect().width); // => 実際の幅
同じ理由で、スクリプトを <head> 内で同期的に実行していて対象の要素がまだパースされていない場合は、querySelector() の時点で null になります。defer 属性を付けるか、DOMContentLoaded を待つか、</body> の直前で読み込むようにしてください。
また、Web フォントや画像の読み込みが終わる前に測ると、あとから確定する値とずれることがあります。画像を含む要素の高さを測るなら、load イベントを待つか、画像側に width / height 属性を指定して読み込み前から高さが決まるようにしておくと安定します。
呼び出しすぎるとページが重くなる
getBoundingClientRect() は正確な値を返すために、その時点で保留になっているレイアウトの計算をブラウザーに強制します。これがリフロー(レイアウトの再計算)で、要素数が多いページでは無視できないコストになります。
読み取りと書き込みを交互に行わない
特に負荷が高いのは、位置の読み取りとスタイルの変更を交互に繰り返すコードです。スタイルを書き換えるとレイアウトが「未計算」の状態になり、その直後に getBoundingClientRect() を呼ぶと計算をやり直さざるを得ません。これがループの中で何度も起きると、要素の数だけリフローが発生します。
// NG: 測る → 書き込む、を交互に繰り返している
items.forEach((el) => {
const rect = el.getBoundingClientRect(); // 直前の書き込みでレイアウトが無効化される
el.style.height = `${rect.width}px`;
});
// OK: 先にすべて測ってから、まとめて書き込む
const widths = items.map((el) => el.getBoundingClientRect().width);
items.forEach((el, i) => {
el.style.height = `${widths[i]}px`;
});
「読み取りをまとめる、書き込みをまとめる」と意識するだけで、リフローの回数は大きく減らせます。
画面内判定なら IntersectionObserver を検討する
scroll イベントは細かく発火するため、そのたびに getBoundingClientRect() を呼ぶと処理が積み重なります。値を使い続ける必要があるなら requestAnimationFrame で1フレームに1回に間引くのが定番ですが、そもそも用途が「要素が画面に入ったかどうか」だけなら IntersectionObserver という選択肢があります。
IntersectionObserver は、要素とビューポートの交差状態が変わったときだけコールバックを呼んでくれる仕組みです。監視はブラウザー側で行われるので、スクロールのたびに自前で座標を計算する必要がありません。
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-shown');
observer.unobserve(entry.target); // 一度表示したら監視をやめる
}
});
});
document.querySelectorAll('.fade-in').forEach((el) => {
observer.observe(el);
});
スクロールに合わせて表示するアニメーションや画像の遅延読み込みは、この形で書くほうが軽く、コードも短くなります。一方、クリック位置の変換や要素に合わせた配置のようにそのときの正確な座標が必要な処理は getBoundingClientRect() の仕事です。用途で使い分けましょう。
まとめ
getBoundingClientRect() は、要素の位置とサイズを top / right / bottom / left / width / height / x / y として返すメソッドです。座標の基準はビューポートの左上なので、スクロールすると値が変わります。ページ先頭からの位置がほしいときは rect.top + window.scrollY のようにスクロール量を足してください。返る値は小数を含み、呼び出した時点のスナップショットである点も覚えておきましょう。
offsetWidth はレイアウト上のサイズを整数で返し transform の影響を受けないのに対し、getBoundingClientRect() は画面に見えているサイズを小数まで返します。見た目を扱うならこちら、と考えると迷いません。event.clientX - rect.left で要素内の座標を求める書き方は、クリック位置の判定から自作 UI まで幅広く使えます。
値がすべて 0 になるときは、要素が display: none になっていないか、まだ DOM に追加されていないかを確認してください。そして、このメソッドはリフローを伴うため呼びすぎると重くなります。読み取りと書き込みはまとめ、画面内に入ったかどうかを知りたいだけなら IntersectionObserver を使うのが安全です。