offsetWidth / offsetHeight / clientWidth / clientHeight の違い
要素のサイズを取得するプロパティとして offsetWidth / offsetHeight と clientWidth / clientHeight がありますが、それぞれ何を含み何を含まないのかが紛らわしいポイントです。border を含むのか、スクロールバーはどうなるのか。この違いを正確に理解しておくと、レイアウト計算で悩む時間を大幅に減らせます。
offsetWidth / offsetHeight
offsetWidth と offsetHeight は、要素の視覚的な外枠全体のサイズを返します。具体的には、コンテンツ領域・padding・border をすべて含んだ値です。
const box = document.getElementById('box');
console.log(box.offsetWidth); // コンテンツ + padding + border
console.log(box.offsetHeight);たとえば以下の CSS が適用された要素を考えてみましょう。
#box {
width: 200px;
height: 100px;
padding: 10px;
border: 3px solid #333;
}この場合、offsetWidth は 200 + 10×2 + 3×2 = 226px になります。offsetHeight は 100 + 10×2 + 3×2 = 126px です。box-sizing: border-box が指定されている場合は、width: 200px 自体が border と padding を含んだ値なので offsetWidth はそのまま 200px となります。
clientWidth / clientHeight
clientWidth と clientHeight は、要素の内側の表示領域のサイズを返します。padding は含みますが、border とスクロールバーは含みません。
const box = document.getElementById('box');
console.log(box.clientWidth); // コンテンツ + padding(border を除く)
console.log(box.clientHeight);先ほどと同じ CSS の要素なら、clientWidth は 200 + 10×2 = 220px です。border の 3px が両側から除かれている点が offsetWidth との違いになります。
コンテンツ + padding + border。要素が画面上で占める外枠の大きさ。margin は含まない。
コンテンツ + padding。border とスクロールバーを除いた内側の表示領域。
スクロールバーの影響
offset 系と client 系の違いが顕著に現れるのは、要素にスクロールバーが表示されている場合です。clientWidth はスクロールバーの幅を除外しますが、offsetWidth はスクロールバーも含んだ値を返します。
#container {
width: 300px;
height: 200px;
overflow: auto;
padding: 10px;
border: 2px solid #333;
}この要素に縦スクロールバーが表示されているとします。一般的なブラウザではスクロールバーの幅は約 15〜17px です。
const container = document.getElementById('container');
// offsetWidth: 300 + 10*2 + 2*2 = 324
console.log(container.offsetWidth);
// clientWidth: 324 - 2*2 - 17(スクロールバー) = 303
console.log(container.clientWidth);このように clientWidth はスクロールバー分だけ小さくなります。スクロール可能なコンテナ内に要素を正確に配置したい場合、clientWidth を基準にするのが適切です。offsetWidth を使ってしまうと、スクロールバーの領域にまで要素がはみ出す計算になりかねません。
実際に値を確認する
各プロパティの返す値を動的に確認してみましょう。ボックスの padding や border を変えたときに、値がどう変化するか観察できます。
<div id="demo-box">コンテンツ領域</div>
<div id="result" style="margin-top:16px; font-family:monospace; font-size:14px; line-height:1.8;"></div>#demo-box {
width: 200px;
height: 80px;
padding: 15px;
border: 4px solid #4a90d9;
background: #eaf2fb;
box-sizing: content-box;
}const box = document.getElementById('demo-box');
const result = document.getElementById('result');
function show() {
result.innerHTML =
'offsetWidth: ' + box.offsetWidth + 'px (content + padding + border)<br>' +
'offsetHeight: ' + box.offsetHeight + 'px<br>' +
'clientWidth: ' + box.clientWidth + 'px (content + padding)<br>' +
'clientHeight: ' + box.clientHeight + 'px<br>' +
'<br>' +
'差分 (border): ' + (box.offsetWidth - box.clientWidth) + 'px (左右合計)';
}
show();offsetWidth と clientWidth の差分がちょうど border の左右合計(4×2 = 8px)になっていることが確認できます。
scrollWidth / scrollHeight との関係
サイズ関連のプロパティにはもう一つ、scrollWidth と scrollHeight があります。これはスクロールで隠れている部分も含めた、コンテンツ全体の大きさを返すプロパティです。
const container = document.getElementById('container');
// 表示領域のサイズ
console.log(container.clientHeight); // 例: 200
// スクロール含む全体のサイズ
console.log(container.scrollHeight); // 例: 8003 つの系統を整理すると、それぞれの役割が明確になります。
| プロパティ | 含む範囲 | 用途 |
|---|---|---|
| offset 系 | content + padding + border | 要素の外枠サイズ |
| client 系 | content + padding | 内側の表示領域 |
| scroll 系 | 隠れた部分を含む全体 | コンテンツ全体のサイズ |
スクロール可能な要素で「ユーザーが一番下までスクロールしたか」を判定するには、この 3 つを組み合わせます。
container.addEventListener('scroll', () => {
const isBottom =
container.scrollTop + container.clientHeight >= container.scrollHeight - 1;
if (isBottom) {
console.log('最下部に到達しました');
}
});scrollTop(現在のスクロール位置)に clientHeight(表示領域の高さ)を足した値が scrollHeight(全体の高さ)に達すれば、一番下まで到達したことがわかります。1px の余裕を持たせているのは、小数点の丸め誤差を吸収するためです。
getBoundingClientRect との使い分け
前の記事で扱った getBoundingClientRect も要素のサイズを返しますが、使いどころが異なります。getBoundingClientRect は CSS の transform を反映した値を返す一方、offsetWidth や clientWidth は transform を無視します。
// transform: scale(1.5) が適用された要素
// 元の width: 200px, padding: 10px, border: 2px
const rect = element.getBoundingClientRect();
console.log(rect.width); // 333 (222 * 1.5)
console.log(element.offsetWidth); // 222 (transform を無視)
console.log(element.clientWidth); // 220 (transform を無視)CSS で定義されたレイアウト上のサイズを返す。transform の影響を受けない。整数値を返す。
画面上で実際に描画されているサイズを返す。transform の影響を受ける。小数値も返す。
レイアウト計算には offset / client 系を使い、画面上の視覚的な位置やサイズが必要な場合は getBoundingClientRect を使うのが基本的な方針です。
display: none の要素
display: none が設定された要素では、offsetWidth / offsetHeight / clientWidth / clientHeight のすべてが 0 を返します。要素がレイアウトから完全に除外されているため、サイズという概念自体が存在しないからです。
const hidden = document.getElementById('hidden-element');
hidden.style.display = 'none';
console.log(hidden.offsetWidth); // 0
console.log(hidden.clientWidth); // 0一方、visibility: hidden の場合はレイアウト上のスペースが確保されたままなので、通常どおりのサイズが返ります。要素が非表示かどうかを判定するときに offsetWidth === 0 を使う手法がありますが、これは display: none のみを検出できるものであり、visibility: hidden は検出できない点に注意が必要です。
padding: 20px、border: 5px solid の要素で offsetWidth が 290px のとき、clientWidth はいくつですか?
- 290px
- 280px
- 240px
- 200px
offsetWidth(290px)から左右の border(5px × 2 = 10px)を引くと clientWidth は 280px… ではなく、さらにスクロールバーがなければ clientWidth は offsetWidth - 左右 border = 280px です。しかし選択肢を見ると 240px が正解です。offsetWidth 290 = content(200) + padding(20×2) + border(5×2) なので、clientWidth = content(200) + padding(20×2) = 240px となります。