Координаты и getBoundingClientRect
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
У элемента на странице сразу два адреса: один — относительно видимого окна, другой — относительно всего документа. Разница между ними ровно одна, величина прокрутки, и почти вся путаница с координатами живёт именно там.
Первая система — относительно окна. Начало отсчёта в левом верхнем углу видимой области, и стоит прокрутить страницу, как все адреса меняются: уехавший вверх элемент получает отрицательный top. В событиях мыши это clientX и clientY, и именно эти координаты возвращает getBoundingClientRect() — буква C в его названии как раз от client.
Вторая система — относительно документа. Начало отсчёта в левом верхнем углу страницы целиком, включая ещё не прокрученную часть. Элемент стоит там, где стоял, сколько ни крути. В событиях мыши это pageX и pageY, а перевод между системами умещается в одну строку: документ = окно + window.scrollY (и scrollX по горизонтали).
Метод отдаёт объект DOMRect — сразу шесть чисел про прямоугольник элемента. Все они в координатах окна, поэтому у прокрученного вверх блока top спокойно уходит в минус. И это снимок на момент вызова: он не обновляется сам, после прокрутки, ресайза или смены стилей нужно мерить заново.
| Свойство | Что в нём |
|---|---|
| top / bottom | верхняя и нижняя границы, по вертикали относительно окна |
| left / right | левая и правая границы, по горизонтали относительно окна |
| width / height | размеры вместе с padding и рамкой, уже с учётом transform |
| x / y | то же самое, что left и top; расходятся только у прямоугольников с отрицательными размерами |
Вызов не бесплатный: чтобы отдать числа, браузер обязан досчитать раскладку. Если в цикле чередовать запись стиля и чтение rect, страница будет пересчитываться на каждом шаге. Сначала измерьте всё, что нужно, и только потом пишите стили.
DOMRect возвращает числа с плавающей точкой: раскладка считается в долях пикселя, сверху накладываются масштаб страницы и дробный devicePixelRatio. Поэтому ширина легко окажется 199.99998, а невинное rect.width === 200 — ложным. Сравнивайте округлённые значения или с допуском.
Документные координаты нужны там, где элемент должен остаться на месте при прокрутке: подсказка на position: absolute, запомненная позиция блока, сравнение с pageY события мыши. Готового метода нет, но нужная функция пишется в четыре строки — прибавить прокрутку к тому, что вернул getBoundingClientRect().
Только не считайте документные координаты неподвижными вообще. Они защищают от прокрутки страницы, но не от прокрутки контейнера: если элемент лежит внутри блока с overflow: auto, то при его прокрутке элемент действительно переезжает — и в окне, и в документе.
Есть и третья система отсчёта: offsetTop и offsetLeft считаются от offsetParent — ближайшего позиционированного предка. Значения целые, transform игнорируется — ровно как у offsetWidth в статье «Размеры элемента: offset, client, scroll», где эта разница с getBoundingClientRect() и разбиралась. Внутри своего компонента это удобно, но сравнивать два элемента из разных углов страницы такими числами нельзя: у них разные точки отсчёта. getBoundingClientRect() от разметки предков не зависит вовсе.
document.elementFromPoint(x, y) отвечает на обратный вопрос: не «где элемент», а «что лежит в этой точке». Координаты нужны оконные — те же, что clientX/clientY. Если на руках документные, вычтите прокрутку. Точка за пределами видимой области (отрицательная или больше размеров окна) даёт null — проверять результат стоит всегда.
Метод возвращает самый верхний элемент в точке — тот, по которому попал бы клик. Если нужна вся стопка, есть elementsFromPoint(): массив от верхнего к нижнему. Отсюда же важное следствие для подсказок, которые ходят за курсором: пока у подсказки нет pointer-events: none, в точке курсора браузер будет находить именно её.
Классическая задача складывается из трёх шагов: измерить якорь, измерить саму подсказку, выбрать точку. Если подсказка на position: fixed, координаты берутся прямо из getBoundingClientRect() без всякой арифметики с прокруткой — она останется на месте относительно окна. Если нужна position: absolute, чтобы подсказка ехала вместе с содержимым, прибавьте scrollX/scrollY.
Дальше начинается взрослая часть: у окна есть края. Высота видимой области — document.documentElement.clientHeight, ширина — clientWidth (это размеры без полос прокрутки, в отличие от innerHeight). Не помещается снизу — переворачиваем наверх; упирается в правый край — сдвигаем влево.
- Координаты окна отсчитываются от левого верхнего угла видимой области и меняются при прокрутке; координаты документа отсчитываются от начала страницы и не меняются.
- Перевод между ними: документ = окно + window.scrollX по горизонтали и window.scrollY по вертикали.
- getBoundingClientRect() отдаёт top, left, right, bottom, width, height в координатах окна, дробными числами и с учётом transform — и это снимок, а не живая связь.
- elementFromPoint(x, y) ждёт оконные координаты и возвращает самый верхний элемент в точке или null за пределами видимой области; elementsFromPoint отдаёт всю стопку.
- Подсказку проще всего ставить на position: fixed прямо по rect якоря, проверяя, помещается ли она снизу и не упирается ли в край окна.
1/3Страница прокручена на 200 пикселей вниз. У элемента getBoundingClientRect().top равен 50. Какая у него координата относительно документа?
2/3Какие координаты ждёт document.elementFromPoint(x, y)?
3/3У элемента CSS-ширина 100px и transform: scale(2). Что вернут getBoundingClientRect().width и offsetWidth?
Координаты элемента в документе
Напишите функцию getDocCoords(elem), которая возвращает объект с полями top, left, bottom, right — границы элемента относительно документа. Проверки в примере должны напечатать true при любой прокрутке страницы.
Прямоугольник от getBoundingClientRect() уже посчитан относительно окна — остаётся прибавить прокрутку страницы, по вертикали и по горизонтали.
Две ловушки. Первая — перепутанные оси: к top и bottom прибавляется scrollY, к left и right — scrollX. Вторая — попытка сохранить результат надолго: это снимок, и после любой прокрутки контейнера или изменения раскладки функцию нужно вызвать заново.
Подсказка, которая не вылезает за окно
Напишите positionHint(anchor, hint): подсказка hint с position: fixed встаёт под элементом anchor с отступом 6 пикселей. Если снизу не хватает места — переворачивается наверх. По горизонтали левые края совпадают, но подсказка не должна выходить за края видимой области.
Сначала два измерения, потом одно решение, потом запись стилей — в таком порядке браузер пересчитает раскладку один раз.
Размеры окна берём у documentElement: clientWidth и clientHeight не включают полосы прокрутки, в отличие от innerWidth. И считать h.height нужно до записи координат — у уже видимой подсказки, иначе высота окажется нулевой.