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 との違いになります。

offsetWidth / offsetHeight

コンテンツ + padding + border。要素が画面上で占める外枠の大きさ。margin は含まない。

clientWidth / clientHeight

コンテンツ + 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 を変えたときに、値がどう変化するか観察できます。

HTML
CSS
JavaScript
<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);  // 例: 800

3 つの系統を整理すると、それぞれの役割が明確になります。

プロパティ含む範囲用途
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 を無視)
offset / client 系

CSS で定義されたレイアウト上のサイズを返す。transform の影響を受けない。整数値を返す。

getBoundingClientRect

画面上で実際に描画されているサイズを返す。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
__RESULT__

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 となります。