Размеры элемента: offset, client, scroll

геометрия≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

У элемента на странице нет одной ширины — их сразу три. offsetWidth меряет блок снаружи, clientWidth — внутреннюю область без рамок, а scrollWidth знает и про то содержимое, которое уехало за край.

Разделы темы:·

Самая внешняя мерка. offsetWidth — это вся видимая коробка элемента: содержимое, внутренние отступы padding и рамки border. Ровно то, что вы обвели бы пальцем по краю блока. offsetHeight — то же самое по вертикали.

Это число, а не строка: браузер уже всё посчитал и округлил до целых пикселей. Считать ничего не надо, читать можно сразу.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

box-sizing: content-box в примере стоит явно, как и в остальных примерах статьи с рамками и отступами. Песочница учебника, как многие сбросы стилей, ставит всем элементам border-box, а при нём width: 200px — уже вся коробка вместе с padding и рамками, и разница вышла бы нулевой. Чем две модели отличаются, разобрано в прологе: что входит в width при content-box и border-box.

Внешние отступы margin в offset не входят: он про сам блок, а не про воздух вокруг него. Так что «место, которое элемент занимает в раскладке» и offsetWidth — не одно и то же.

clientWidth и clientHeight меряют внутреннюю область — содержимое плюс padding, но без рамок. Это ответ на вопрос «сколько места есть внутри блока», а не «сколько блок занимает снаружи».

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Есть второй вычитаемый: полоса прокрутки. Она рисуется внутри рамок и отнимает место у содержимого, поэтому offsetWidth - clientWidth — это не всегда только рамки. Насколько толстая полоса, зависит не от браузера, а от системы и её настроек: в Windows она обычно съедает около пятнадцати пикселей, в macOS с накладными полосами — ничего, а в той же macOS, где полосы велено показывать всегда, снова те же пятнадцать. Именно поэтому её ширину считают на живом элементе, а не хранят константой.

scrollWidth и scrollHeight отвечают на вопрос «сколько там содержимого целиком» — вместе с той частью, что не поместилась. Если всё влезает, scroll-размеры совпадают с client; как только появился излишек, scroll становится больше.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Последняя строка — рабочий рецепт «докрутили до низа». Допуск в пару пикселей нужен потому, что scrollTop бывает дробным, а scrollHeight округляется: точное равенство иногда не срабатывает на масштабе страницы, отличном от ста процентов.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Запоминается проще всего через то, что во что входит.

Свойстваborderpaddingсодержимое за краемполоса прокрутки
offsetWidth / offsetHeightвходитвходитнетвходит (она внутри рамок)
clientWidth / clientHeightнетвходитнетнет
scrollWidth / scrollHeightнетвходитданет
js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

offset — снаружи, client — внутри рамок, scroll — вместе с тем, что уехало за край

Соблазн взять ширину из стилей велик, но elem.style.width знает только про инлайновый атрибут style. Ширина, заданная в таблице стилей, туда не попадает — вы получите пустую строку и решите, что ширины нет.

getComputedStyle честнее, но отдаёт строку вроде «120px», которую надо разбирать через parseFloat, и описывает CSS-модель, а не итоговую коробку: рамки, padding и полоса прокрутки в это число не входят, а box-sizing меняет его смысл. Плюс у элемента, который не отрисован (display: none), вместо пикселей может прийти auto или проценты.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Геометрические свойства смотрят на результат, который браузер уже разложил: шрифты, переносы строк, соседей, появившуюся полосу прокрутки. Правило простое: CSS — чтобы задавать, offset/client/scroll — чтобы измерять.

Он даёт дробные размеры и, в отличие от offset, учитывает transform — то есть меряет то, что реально видно на экране. Если элемент масштабирован или повёрнут, честный ответ даст именно rect.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Этим методом меряет ширину и высоту прибор пролога: measure(".box", "width") печатает не offsetWidth, а ширину прямоугольника, округлённую до сотых пикселя. Поэтому у масштабированной коробки прибор показывает размер уже после трансформации.

Все геометрические свойства скрытого элемента равны нулю. Не «примерно», не «как было в прошлый раз» — ровно 0. То же самое у элемента, который ещё не вставлен в документ: пока браузер его не разложил, мерить нечего.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Обратите внимание на предпоследнюю строку: CSS уверенно называет ширину у элемента, которого на странице нет. Отсюда и самый частый баг — размеры снимают до того, как убрали display: none, получают нули и делят на них. Правильный порядок: сначала показать, потом читать offset/client/scroll — браузер пересчитает раскладку прямо в момент чтения, синхронно.

Если размеры нужны до появления элемента, прячьте его не через display: none, а через visibility: hidden: место в раскладке он занимает и размеры отдаёт. Чтобы он при этом не распирал страницу, добавьте position: absolute: элемент выпадет из потока и соседей не подвинет. Ширину тогда задайте сами: абсолютный блок без width сжимается по содержимому, и замер может не совпасть с шириной, которая будет у него в потоке.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

  • offsetWidth и offsetHeight — вся коробка элемента: содержимое, padding и рамки; margin не входит.
  • clientWidth и clientHeight — внутренняя область: содержимое и padding, без рамок и без полосы прокрутки.
  • scrollWidth и scrollHeight знают про содержимое целиком; scrollHeight - scrollTop - clientHeight показывает, сколько осталось прокрутить, а scrollWidth > clientWidth — что текст обрезан.
  • CSS для измерений не годится: style.width видит только инлайновые стили, getComputedStyle отдаёт строку и не учитывает рамки, padding и полосу прокрутки.
  • У элемента с display: none и у элемента вне документа все размеры равны нулю — сначала показать, потом мерить.

1/3У блока width: 100px, padding: 10px, border: 5px и box-sizing: content-box. Чему равен offsetWidth?

2/3Текст в блоке с overflow: hidden. Как понять, что он обрезан?

3/3Что вернёт offsetHeight у элемента с display: none?

Сколько осталось прокрутить

Допишите две функции. restToScroll(el) возвращает, сколько пикселей содержимого ещё скрыто ниже видимой области, а atBottom(el) отвечает, докручен ли блок до самого низа. Помните про дробный scrollTop: точное равенство здесь ненадёжно.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

restToScroll — это el.scrollHeight - el.scrollTop - el.clientHeight: вся высота содержимого минус уже прокрученное минус видимое окно.

atBottom удобно выразить через первую функцию: return restToScroll(el) < 2; Допуск нужен потому, что scrollTop бывает дробным при нестандартном масштабе страницы, а scrollHeight и clientHeight округлены до целых — строгое === 0 иногда не наступает никогда.

Толщина полосы прокрутки

Полоса прокрутки рисуется внутри рамок и отъедает место у содержимого. Вычислите её толщину для конкретного блока, не заглядывая в настройки системы. Подсказка: clientTop — это толщина верхней рамки, а рамка задана одним значением на все стороны.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Из горизонтальной разницы вычитаем рамки: col.offsetWidth - col.clientWidth - 2 * col.clientTop. Вертикальная разница (offsetHeight - clientHeight) для этого не годится — горизонтальной полосы у блока нет, там окажутся одни рамки.

Результат зависит от системы: в Windows и Linux это обычно около 15, в macOS с накладными полосами — 0. Поэтому число всегда считают на живом элементе, а не зашивают в код.