jQuery / メソッド / .offset() - 文書基準の座標を取得・設定する

jQueryの.offset()は最初の選択要素のボーダーボックスについて、文書左上からの距離を{ top, left }で返します。.offset({ top, left })は選択した要素をその文書座標へ移動します。表示領域であるビューポートや親基準の位置と混同しません。

offset・position・ビューポートの違い

.offset()の仕様は文書を基準に、対象のmarginを含まないボーダーボックスを測ります。top・leftはCSSピクセル単位の数値で、小数の場合もあります。取得だけでは移動しません。

方法 基準 対象の境界
.offset() 文書 ボーダーボックス
.position() offset親のパディングボックス 対象のマージンボックス
getBoundingClientRect() ビューポート 境界線を含む矩形

.position()のoffset親はHTML上の直接の親とは限りません。getBoundingClientRect()のtop・leftはスクロールで変わり、通常の文書座標へはwindow.scrollY・scrollXを加えます。異なる基準の値をそのままCSS top・leftに入れません。

三つの座標とスクロールを比較する例

全コードをUTF-8のoffset-demo.htmlとして保存し、ブラウザーで開きます。コンテンツ配信ネットワーク(CDN)から公式jQueryファイル4.0.0を読むインターネットが必要ですが、インストールは不要です。取得・スクロール・移動・非表示・初期化と以下の関数設定を3.7.1と4.0.0で実行しました。

stageは文書top 500・left 20で境界線5です。その中のtargetはtop 40・left 50、margin 10です。body marginは0で変形はなく、直接計算できます。操作部は画面上部に固定します。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Document and viewport coordinates</title>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
  <style>
    body { margin: 0; min-height: 1800px; font: 16px/1.6 sans-serif; }
    #controls { position: fixed; inset: 0 0 auto; padding: 12px; background: #fff; z-index: 10; max-height: 42vh; overflow: auto; }
    button { font: inherit; margin: 3px; }
    pre { white-space: pre-wrap; overflow-wrap: anywhere; }
    #stage { position: absolute; top: 500px; left: 20px; width: 300px; height: 260px; border: 5px solid #bbc; }
    #target { position: absolute; top: 40px; left: 50px; margin: 10px; border: 2px solid teal; padding: 8px; background: #d9f4ef; }
  </style>
</head>
<body>
  <div id="controls">
    <strong>Measure the target border box</strong>
    <button type="button" id="read">Read</button>
    <button type="button" id="scroll">Scroll 200px</button>
    <button type="button" id="move">Move to (top 620, left 130)</button>
    <button type="button" id="visibility">Toggle visibility</button>
    <button type="button" id="display">Toggle display</button>
    <button type="button" id="reset">Reset</button>
    <pre id="result" aria-live="polite"></pre>
  </div>
  <div id="stage"><div id="target">Target</div></div>
  <script>
    $(function () {
      const $target = $("#target");
      function n(value) { return Number(value.toFixed(2)); }
      function report() {
        const node = $target.get(0);
        if (!node) { $("#result").text("Target missing"); return; }
        if (!node.getClientRects().length) { $("#result").text("Not rendered; reveal before measuring"); return; }
        const offset = $target.offset();
        const rect = node.getBoundingClientRect();
        const position = $target.position();
        $("#result").text(`document top=${n(offset.top)}, left=${n(offset.left)}\nviewport top=${n(rect.top)}, left=${n(rect.left)}\nposition top=${n(position.top)}, left=${n(position.left)}\nscrollY=${n(window.scrollY)}`);
      }
      $("#read").on("click", report);
      $("#scroll").on("click", function () { window.scrollTo(0, 200); });
      $("#move").on("click", function () { if ($target.get(0).getClientRects().length) $target.offset({ top: 620, left: 130 }); report(); });
      $("#visibility").on("click", function () { $target.css("visibility", $target.css("visibility") === "hidden" ? "visible" : "hidden"); report(); });
      $("#display").on("click", function () { $target.toggle(); report(); });
      $("#reset").on("click", function () { $target.css({ top: 40, left: 50, display: "", visibility: "" }); window.scrollTo(0, 0); report(); });
      $(window).on("scroll.offsetLesson resize.offsetLesson", report);
      report();
    });
  </script>
</body>
</html>

スクロール0の初期結果です。他ページに一部だけを入れると周囲の配置が変わり、同じ数値とは限りません。

document top=555, left=85
viewport top=555, left=85
position top=40, left=50
scrollY=0
  1. Readで再取得します。文書topは500 + 5 + 40 + 10 = 555、leftは20 + 5 + 50 + 10 = 85です。
  2. Scroll 200pxでは文書topは555、viewport topは355、scrollYは200です。.position()は40・50を保ちます。
  3. Move to (top 620, left 130)で文書座標620・130にします。スクロール200ではviewportは420・130、.position()は105・95となり、親基準CSSとは違うと分かります。
  4. Toggle visibilityは見えなくても配置ボックスが残り、取得できます。
  5. Toggle displayでdisplay:noneになると数値の代わりにNot renderedを出します。再度押すと取得できます。
  6. ResetはCSS位置と表示状態を戻し、スクロールを0にします。

対象は通常の絶対配置要素です。文書内位置が変わらなければウィンドウスクロールだけで文書座標は変わりませんが、fixed・stickyや配置変化に一律適用しません。画像、フォント、画面サイズ変更後は必要な時点で再計測します。

座標設定と関数型引数

.offset({ top: 620, left: 130 })は文書座標を受け、親内のCSS top・leftではありません。jQueryが必要値を計算し、static要素ではrelativeに変更する場合があります。CSSと配置への影響も確認します。

.offset(function(index, current))は各要素の現在の文書座標を受け、新top・leftを返します。開発者ツールのConsoleで次の完全な部分コードを実行します。一時要素二つに異なる縦増分を与え、最後に整理します。

const $items = $("<div>A</div><div>B</div>")
  .css({ position: "absolute", top: 1000, left: 20 }).appendTo("body");
$items.offset(function (index, current) {
  return { top: current.top + index * 30, left: current.left + 40 };
});
console.log($items.map(function () {
  const point = $(this).offset();
  return `${point.top},${point.left}`;
}).get().join(" | "));
console.log(`empty getter=${typeof $().offset()}`);
$items.remove();
1000,60 | 1030,60
empty getter=undefined

取得は最初だけ、設定は全選択要素に適用します。indexは集合内で0からです。例の丸めは表示用であり、計算時に小数を無条件に捨てません。

非表示・空の選択・配置変化

display:noneは描画ボックスがなく、意味のある現在位置の取得対象にしません。0などが返っても実位置と見なさず、表示してから測ります。visibility:hiddenはボックスが残るため取得できます。

例はノードの存在とgetClientRects().lengthを先に確認します。空の選択の.offset()はundefinedで、直ちに.offset().topを使うと失敗します。DOM(Document Object Model、文書オブジェクトモデル)から外れたノードにも意図した文書位置を期待しません。

ズーム、transform、margin・border変更、祖先の配置を確認します。公式仕様はhtml要素のmargin計算をサポートしません。まず座標基準を合わせ、不要な計測とCSS書き込みを繰り返さず必要な配置変化に合わせて更新します。