Пошук уроків, статей та іншого контенту
Навчитеся вимірювати елементи, визначати їхнє розташування та працювати з прокручуванням сторінки.
У браузері JavaScript часто має визначити:
фактичну ширину та висоту елемента;
його положення у вікні браузера або на сторінці;
чи видно елемент у viewport;
наскільки прокручений документ;
чи можна прокрутити внутрішній контейнер;
як прокрутити сторінку до певного елемента.
Це потрібно для реалізації модальних вікон, підказок, нескінченного прокручування, «липких» елементів, анімацій під час появи елементів і перевірки видимості.
Розглянемо HTML-елемент:
<div class="card">Вміст картки</div>Його розмір може описуватися кількома властивостями.
offsetWidth та offsetHeightoffsetWidth і offsetHeight повертають розмір елемента в пікселях, округлений до цілого числа.
До розміру входять:
вміст;
внутрішні відступи (padding);
межі (border);
вертикальна смуга прокручування, якщо вона є.
Зовнішні відступи (margin) не враховуються.
const card = document.querySelector('.card');
console.log(card.offsetWidth);
console.log(card.offsetHeight);Ці властивості корисні, коли потрібен фізичний розмір елемента разом із його рамкою.
clientWidth та clientHeightclientWidth і clientHeight повертають розмір внутрішньої області елемента.
До нього входять:
вміст;
padding.
Не входять:
border;
margin;
зазвичай ширина вертикальної смуги прокручування.
console.log(card.clientWidth);
console.log(card.clientHeight);Для звичайного елемента без внутрішньої прокрутки приблизно виконується:
clientWidth = ширина вмісту + лівий padding + правий padding
offsetWidth = clientWidth + ліва межа + права межаОднак точне значення залежить від напрямку прокручування та особливостей браузера.
scrollWidth та scrollHeightscrollWidth і scrollHeight описують повний розмір вмісту, включно з частиною, яка зараз прихована через прокручування.
const container = document.querySelector('.container');
console.log(container.clientWidth);
console.log(container.scrollWidth);
console.log(container.clientHeight);
console.log(container.scrollHeight);Якщо scrollHeight більший за clientHeight, усередині елемента є вертикальний вміст, який не поміщається без прокручування.
const hasVerticalScroll =
container.scrollHeight > container.clientHeight;
console.log(hasVerticalScroll);Порівняння scrollWidth і clientWidth дає змогу перевірити горизонтальну прокрутку:
const hasHorizontalScroll =
container.scrollWidth > container.clientWidth;box-sizingЗа замовчуванням браузер використовує:
box-sizing: content-box;За такого режиму width і height задають лише розмір області вмісту. padding і border додаються до нього.
.card {
width: 200px;
padding: 20px;
border: 5px solid steelblue;
}Фактична ширина без урахування округлення буде:
200 + 20 + 20 + 5 + 5 = 250 пікселівЯкщо використати:
.card {
box-sizing: border-box;
width: 200px;
padding: 20px;
border: 5px solid steelblue;
}то ширина разом із padding і border буде 200 пікселів.
Властивості JavaScript, такі як offsetWidth, вимірюють фактичний результат CSS-розкладки, а не лише значення width.
getBoundingClientRect()Метод getBoundingClientRect() повертає об'єкт із геометрією елемента:
const card = document.querySelector('.card');
const rectangle = card.getBoundingClientRect();
console.log(rectangle);Об'єкт містить, зокрема:
width — ширина елемента;
height — висота елемента;
top — відстань від верхнього краю viewport;
right — координата правого краю відносно viewport;
bottom — координата нижнього краю відносно viewport;
left — відстань від лівого краю viewport;
x — горизонтальна координата;
y — вертикальна координата.
На відміну від offsetWidth та offsetHeight, значення width і height можуть бути дробовими.
const rect = card.getBoundingClientRect();
console.log(`Ширина: ${rect.width}px`);
console.log(`Висота: ${rect.height}px`);
console.log(`Відстань від верху viewport: ${rect.top}px`);
console.log(`Відстань від лівого краю viewport: ${rect.left}px`);Viewport — це видима область сторінки у вікні браузера.
Якщо верхня частина елемента розташована на 100 пікселів нижче верхнього краю видимої області, то:
element.getBoundingClientRect().top === 100;Після прокручування сторінки значення top може змінитися, навіть якщо елемент не переміщується в документі.
Наприклад, якщо елемент був нижче видимої області, а користувач прокрутив сторінку вниз, його top зменшиться.
getBoundingClientRect() повертає координати відносно viewport, а не всієї сторінки.
Щоб отримати координати відносно документа, потрібно додати поточну прокрутку:
const rect = element.getBoundingClientRect();
const documentTop = rect.top + window.scrollY;
const documentLeft = rect.left + window.scrollX;
console.log(documentTop);
console.log(documentLeft);Це важливо для позиціонування елементів, які мають бути прив'язані до сторінки, а не до видимої області.
Наприклад, координати верхнього краю документа можна отримати так:
const pageTop =
element.getBoundingClientRect().top + window.scrollY;Не варто використовувати document.body.scrollTop або document.documentElement.scrollTop як універсальний варіант. Для прокручування вікна надійніше використовувати window.scrollY і window.scrollX.
offsetTop та offsetLeftoffsetTop і offsetLeft повертають відстань елемента до верхнього або лівого краю його offsetParent.
console.log(element.offsetTop);
console.log(element.offsetLeft);
console.log(element.offsetParent);offsetParent — це найближчий предок, відносно якого браузер обчислює ці координати. Ним може бути:
позиціонований предок;
таблиця;
body;
інший елемент, залежно від структури та CSS.
Тому offsetTop не завжди є координатою відносно всього документа. Якщо потрібна стабільна геометрія у viewport, краще використовувати getBoundingClientRect().
Поточну вертикальну прокрутку документа можна отримати через window.scrollY:
console.log(window.scrollY);Горизонтальна прокрутка доступна через window.scrollX:
console.log(window.scrollX);У сучасних браузерах також доступні:
console.log(window.pageYOffset);
console.log(window.pageXOffset);Проте для нового коду зазвичай використовують scrollY і scrollX.
Метод window.scrollTo() прокручує документ до заданих координат:
window.scrollTo(0, 500);Сторінка прокрутиться так, щоб її горизонтальна координата була 0, а вертикальна — 500 пікселів.
Цей метод також приймає об'єкт налаштувань:
window.scrollTo({
top: 500,
left: 0,
behavior: 'smooth'
});Властивість behavior може мати значення:
'auto' — миттєва прокрутка;
'smooth' — плавна прокрутка.
Метод window.scrollBy() прокручує документ відносно поточної позиції:
window.scrollBy({
top: 300,
left: 0,
behavior: 'smooth'
});На відміну від scrollTo(), цей метод не задає абсолютну координату.
Метод scrollIntoView() прокручує найближчий контейнер або сторінку так, щоб елемент став видимим.
const section = document.querySelector('#features');
section.scrollIntoView({
behavior: 'smooth',
block: 'start'
});Властивість block керує вертикальним вирівнюванням:
'start' — верх елемента буде біля верхнього краю;
'center' — елемент буде по центру;
'end' — низ елемента буде біля нижнього краю;
'nearest' — браузер виконає мінімальну необхідну прокрутку.
Властивість inline керує горизонтальним вирівнюванням:
section.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
});Якщо на сторінці є фіксована шапка, після scrollIntoView({ block: 'start' }) вона може закрити верхню частину секції.
Для такого випадку можна задати CSS-властивість:
section {
scroll-margin-top: 80px;
}Тепер браузер залишатиме зверху 80 пікселів під час прокручування до секції.
Прокручуватися може не лише весь документ, а й окремий елемент.
.messages {
width: 300px;
height: 200px;
overflow-y: auto;
}Для такого контейнера:
const messages = document.querySelector('.messages');
console.log(messages.scrollTop);
console.log(messages.scrollLeft);scrollTop показує, наскільки вертикально прокручено вміст елемента.
Прокрутити контейнер можна так:
messages.scrollTo({
top: messages.scrollHeight,
behavior: 'smooth'
});Цей приклад прокручує контейнер до кінця.
Метод scrollBy() працює і для окремих елементів:
messages.scrollBy({
top: 100,
behavior: 'smooth'
});Для прокручування до дочірнього елемента можна використати scrollIntoView(), але потрібно враховувати, що браузер може прокрутити не лише внутрішній контейнер, а й сторінку.
Найпростіша перевірка видимості у viewport:
function isVisible(element) {
const rect = element.getBoundingClientRect();
return (
rect.top < window.innerHeight &&
rect.bottom > 0 &&
rect.left < window.innerWidth &&
rect.right > 0
);
}Перевірка:
const banner = document.querySelector('.banner');
console.log(isVisible(banner));Умова означає:
нижній край елемента нижче верхнього краю viewport;
верхній край елемента вище нижнього краю viewport;
правий край правіше лівого краю viewport;
лівий край лівіше правого краю viewport.
Елемент може бути частково видимим. Якщо потрібно перевірити, чи повністю він поміщається у viewport:
function isFullyVisible(element) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth
);
}Подія scroll виникає під час прокручування документа або прокручуваного елемента.
window.addEventListener('scroll', () => {
console.log(window.scrollY);
});Наприклад, можна показувати кнопку «На початок» лише після прокручування:
const backToTop = document.querySelector('.back-to-top');
window.addEventListener('scroll', () => {
backToTop.hidden = window.scrollY < 400;
});
backToTop.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});Подія scroll може виникати дуже часто. Не слід виконувати в її обробнику важкі операції без потреби.
Для складніших випадків можна обмежувати частоту перевірок через requestAnimationFrame:
let frameRequested = false;
window.addEventListener('scroll', () => {
if (frameRequested) {
return;
}
frameRequested = true;
requestAnimationFrame(() => {
console.log(window.scrollY);
frameRequested = false;
});
});У цьому прикладі обчислення виконуються не частіше одного разу за кадр браузера.
Нижче наведено повний приклад сторінки, яка:
показує поточну позицію прокручування;
показує прогрес прокручування;
відображає кнопку повернення на початок;
прокручує до секції;
виводить координати секції у viewport і документі.
<!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Розміри та прокручування</title>
<style>
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
font-family: sans-serif;
line-height: 1.5;
}
header {
position: sticky;
top: 0;
z-index: 10;
padding: 16px;
color: white;
background: #263238;
}
.progress {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 4px;
transform-origin: left;
transform: scaleX(0);
background: #4fc3f7;
}
main {
max-width: 700px;
margin: 0 auto;
padding: 32px 16px 600px;
}
section {
min-height: 500px;
margin: 24px 0;
padding: 24px;
border: 2px solid #90a4ae;
scroll-margin-top: 80px;
}
.back-to-top {
position: fixed;
right: 20px;
bottom: 20px;
padding: 10px 14px;
}
.metrics {
position: fixed;
right: 20px;
bottom: 70px;
padding: 12px;
color: white;
background: #37474f;
font-size: 14px;
}
</style>
</head>
<body>
<header>
<button id="featuresButton">До можливостей</button>
<div class="progress" id="progress"></div>
</header>
<main>
<h1>Демонстрація геометрії та прокручування</h1>
<p>
Прокрутіть сторінку, щоб побачити зміну координат і прогресу.
</p>
<section id="intro">
<h2>Вступ</h2>
<p>Це перша секція сторінки.</p>
</section>
<section id="features">
<h2>Можливості</h2>
<p>Це секція, до якої можна прокрутити сторінку кнопкою.</p>
</section>
</main>
<div class="metrics" id="metrics"></div>
<button class="back-to-top" id="backToTop" hidden>
На початок
</button>
<script>
const progress = document.querySelector('#progress');
const metrics = document.querySelector('#metrics');
const backToTop = document.querySelector('#backToTop');
const features = document.querySelector('#features');
const featuresButton = document.querySelector('#featuresButton');
function updatePageMetrics() {
const sectionRect = features.getBoundingClientRect();
const documentHeight =
document.documentElement.scrollHeight - window.innerHeight;
const scrollProgress = documentHeight > 0
? window.scrollY / documentHeight
: 0;
const progressValue = Math.min(Math.max(scrollProgress, 0), 1);
progress.style.transform = `scaleX(${progressValue})`;
metrics.textContent = [
`Прокрутка: ${Math.round(window.scrollY)}px`,
`top секції у viewport: ${Math.round(sectionRect.top)}px`,
`top секції у документі: ${
Math.round(sectionRect.top + window.scrollY)
}px`
].join('\n');
backToTop.hidden = window.scrollY < 300;
}
featuresButton.addEventListener('click', () => {
features.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
});
backToTop.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
window.addEventListener('scroll', updatePageMetrics);
window.addEventListener('resize', updatePageMetrics);
updatePageMetrics();
</script>
</body>
</html>resize та зміна розмірівПід час зміни розміру вікна браузера виникає подія resize:
window.addEventListener('resize', () => {
console.log(window.innerWidth, window.innerHeight);
});Розмір viewport можна отримати через:
console.log(window.innerWidth);
console.log(window.innerHeight);Ці значення містять розміри viewport. Для точних розрахунків адаптивного інтерфейсу також важливо повторно вимірювати елементи після resize, оскільки CSS-розкладка могла змінитися.
Якщо JavaScript виконується до створення елемента в DOM, querySelector() поверне null:
const element = document.querySelector('.card');
console.log(element.getBoundingClientRect());Такий код завершиться помилкою, якщо елемента ще немає.
Можливі рішення:
розмістити script перед </body>;
додати defer для зовнішнього скрипту;
виконати код після події DOMContentLoaded.
document.addEventListener('DOMContentLoaded', () => {
const element = document.querySelector('.card');
if (element) {
console.log(element.getBoundingClientRect());
}
});Також елемент може бути прихованим через display: none. У такому разі він не бере участі в розкладці, а його розміри через offsetWidth, offsetHeight і getBoundingClientRect() будуть нульовими.
const top = element.getBoundingClientRect().top;Це координата відносно viewport. Для координати відносно документа додайте window.scrollY:
const documentTop =
element.getBoundingClientRect().top + window.scrollY;offsetTop як універсальну координатуoffsetTop залежить від offsetParent. Якщо в ієрархії зміниться позиціонований предок, результат також може змінитися.
Для вимірювання положення відносно viewport використовуйте getBoundingClientRect().
margin до розміру елементаoffsetWidth і offsetHeight не включають зовнішні відступи.
Якщо потрібно врахувати горизонтальні зовнішні відступи:
const styles = getComputedStyle(element);
const fullWidth =
element.offsetWidth +
parseFloat(styles.marginLeft) +
parseFloat(styles.marginRight);Розмір може бути некоректним, якщо:
DOM ще не побудований;
стилі ще не застосовані;
елемент має display: none;
текст або зображення ще змінили розкладку.
Вимірювання слід виконувати після появи елемента в DOM і, за потреби, після завантаження ресурсів, що впливають на його розмір.
scrollОбробник scroll може викликатися дуже часто. Не варто безпосередньо виконувати в ньому складні обчислення, зміну великої кількості DOM-вузлів або багаторазовий доступ до стилів.
Для частих оновлень використовуйте requestAnimationFrame, а для перевірки появи елементів у viewport розгляньте IntersectionObserver.
Читання геометричних властивостей, наприклад offsetWidth або getBoundingClientRect(), після змін стилів може змусити браузер негайно перерахувати розкладку.
Бажано групувати операції:
спочатку прочитати потрібні розміри;
потім змінити стилі;
не чергувати багаторазово читання і запис у циклі.
offsetWidth і offsetHeight включають вміст, padding і border.
clientWidth і clientHeight включають вміст і padding, але не межі.
scrollWidth і scrollHeight описують повний розмір прокручуваного вмісту.
getBoundingClientRect() повертає точні розміри та координати елемента відносно viewport.
Для координат відносно документа до top або left потрібно додати window.scrollY або window.scrollX.
scrollTo() встановлює абсолютну позицію прокручування.
scrollBy() прокручує відносно поточної позиції.
scrollIntoView() прокручує сторінку або контейнер до елемента.
scrollTop і scrollLeft використовують для внутрішньої прокрутки елементів.
Під час роботи з scroll потрібно враховувати продуктивність.
Для фіксованих шапок корисно використовувати scroll-margin-top.