Ты ставишь элементу width: 200px, добавляешь padding: 20px — и он вдруг стал 240px. Или два блока с margin-bottom: 20px и margin-top: 30px — ожидаешь 50px между ними, а получаешь 30px. Или читаешь element.offsetWidth в JavaScript и получаешь не то, что думаешь. Box Model объясняет всё это.
Браузер вычисляет размеры элементов по Box Model. JavaScript-разработчик работает с этими размерами постоянно: позиционирование, анимации, вычисление доступного пространства. Непонимание box-sizing, margin collapse, и разницы offsetWidth/clientWidth приводит к багам в лейауте.
element.offsetWidth, clientWidth, getBoundingClientRect// content-box (исторический дефолт, ПЛОХОЙ):
// width: 200px + padding: 20px + border: 2px
// Итоговый offsetWidth = 200 + 40 + 4 = 244px (НЕОЖИДАННО!)
// border-box (современный стандарт, ХОРОШИЙ):
// width: 200px включает padding И border
// Итоговый offsetWidth = 200px (предсказуемо!)
// Контент = 200 - 40 - 4 = 156px
// Сброс на border-box (ставь всегда в начале CSS):
// *, *::before, *::after { box-sizing: border-box; }// Вертикальные margin соседних блоков схлопываются:
// .el1 { margin-bottom: 20px }
// .el2 { margin-top: 30px }
// Фактический отступ = max(20, 30) = 30px (не 50!)
// Схлопывание НЕ происходит:
// - в flex/grid контейнерах
// - при padding/border между родителем и первым/последним ребёнком
// - для inline-block элементов
// - горизонтальные margin НИКОГДА не схлопываются// visible — выходит за границы (по умолчанию)
// hidden — обрезается
// scroll — всегда показывает скроллбар
// auto — скроллбар только при необходимости (рекомендуется)
// clip — как hidden, но отключает прокрутку через JS
// overflow-x и overflow-y независимы:
// overflow-x: hidden; overflow-y: auto;
// Паттерны:
// Прокручиваемый список: max-height: 400px; overflow-y: auto;
// Обрезка текста: overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
// Скрытие для анимации: overflow: hidden; (+ transform)// offsetWidth = content + padding + border (без margin)
// clientWidth = content + padding (без border, без scrollbar)
// scrollWidth = полная ширина прокручиваемого контента
// getBoundingClientRect() — точные размеры с дробями:
// { width, height, top, left, right, bottom } — relative to viewport
// Наглядно:
// [margin | border | padding | content | padding | border | margin]
// ^-------- offsetWidth --------^
// ^--- clientWidth ---^
// ^content^
// scrollWidth >= clientWidth (если есть горизонтальный скролл)// Типичные паттерны:
// height: calc(100vh - 64px) — высота минус шапка
// width: calc(100% - 2 * 16px) — ширина минус отступы
// width: calc(50% - 8px) — в 2-колоночном grid с gap
// Из JavaScript:
element.style.width = `calc(100% - ${sidebarWidth}px)`Ошибка 1: Не использовать border-box
/* ПЛОХО — неожиданные размеры */
.input { width: 100%; padding: 12px 16px; }
/* Ширина стала 100% + 32px горизонтальных padding! */
/* ХОРОШО */
*, *::before, *::after { box-sizing: border-box; }
.input { width: 100%; padding: 12px 16px; }
/* Ширина = ровно 100%, padding внутри */Ошибка 2: Удивляться margin collapse
/* Ожидаешь 40px между параграфами, получаешь 20px */
p { margin-top: 20px; margin-bottom: 20px; }
/* Решение: */
p + p { margin-top: 0; }
/* или использовать padding вместо margin на контейнере */Ошибка 3: Путать offsetWidth и clientWidth
// Для позиционирования (включая border):
const fullWidth = element.offsetWidth
// Для внутреннего контента (без border, без scrollbar):
const innerWidth = element.clientWidth
// Для проверки переполнения:
const hasOverflow = element.scrollWidth > element.clientWidthbox-sizing: border-box + width: 100% — без сюрпризовmax-height + overflow-y: auto для выпадающих менюscrollHeight, scrollTop, clientHeight для расчёта видимых элементовoffsetWidth для вычисления размеров при перетаскиванииСимуляция Box Model: вычисление размеров, margin collapse, overflow и scrollable контейнеры
// Симуляция Box Model — чистые вычисления
// ===== Вычисление размеров =====
function calculateBoxModel({ width, height, padding, border, margin, scrollbarWidth = 0 }) {
// --- content-box (исторический) ---
const contentBox = {
contentWidth: width,
contentHeight: height,
offsetWidth: width + 2 * padding + 2 * border,
clientWidth: width + 2 * padding - scrollbarWidth,
totalWidth: width + 2 * padding + 2 * border + 2 * margin,
}
// --- border-box (современный) ---
const borderBox = {
contentWidth: width - 2 * padding - 2 * border,
contentHeight: height - 2 * padding - 2 * border,
offsetWidth: width, // width уже включает всё
clientWidth: width - 2 * border - scrollbarWidth,
totalWidth: width + 2 * margin,
}
return { contentBox, borderBox }
}
// === Демонстрация: content-box vs border-box ===
console.log('=== content-box vs border-box (width=200, padding=20, border=2, margin=16) ===')
const el = { width: 200, height: 100, padding: 20, border: 2, margin: 16 }
const { contentBox, borderBox } = calculateBoxModel(el)
console.log()
console.log('Свойство'.padEnd(18) + 'content-box'.padEnd(14) + 'border-box')
console.log('-'.repeat(44))
const props = ['contentWidth', 'offsetWidth', 'clientWidth', 'totalWidth']
for (const p of props) {
console.log(p.padEnd(18) + String(contentBox[p]).padEnd(14) + String(borderBox[p]))
}
console.log()
console.log('content-box: ожидаешь 200px, получаешь offsetWidth =', contentBox.offsetWidth, '(НЕОЖИДАННО!)')
console.log('border-box: width =', borderBox.offsetWidth, '(ПРЕДСКАЗУЕМО!)')
// ===== Margin Collapse =====
console.log('\n=== Margin Collapse ===')
function verticalGap(mb, mt, isFlex = false) {
// В flex/grid — отступы СКЛАДЫВАЮТСЯ (нет collapse)
return isFlex ? mb + mt : Math.max(mb, mt)
}
const scenarios = [
{ mb: 20, mt: 20, flex: false, desc: 'div(mb:20) + div(mt:20) — обычный поток' },
{ mb: 20, mt: 30, flex: false, desc: 'div(mb:20) + div(mt:30) — обычный поток' },
{ mb: 20, mt: 30, flex: true, desc: 'flex-item(mb:20) + flex-item(mt:30)' },
{ mb: 0, mt: 40, flex: false, desc: 'div(mb:0) + div(mt:40)' },
]
for (const s of scenarios) {
const gap = verticalGap(s.mb, s.mt, s.flex)
const note = s.flex ? '(flex — нет collapse)' : '(collapse: max)'
console.log(`${s.desc}`)
console.log(` ${s.mb}px + ${s.mt}px = ${gap}px ${note}`)
}
// ===== offsetWidth vs clientWidth vs scrollWidth =====
console.log('\n=== offsetWidth / clientWidth / scrollWidth ===')
function simulateScrollElement(containerW, contentW, padding, border) {
// clientWidth: ширина без border, без скроллбара (15px стандарт)
const SCROLLBAR = 15
return {
offsetWidth: containerW,
clientWidth: containerW - 2 * border - SCROLLBAR,
scrollWidth: contentW + 2 * padding,
canScroll: (contentW + 2 * padding) > (containerW - 2 * border - SCROLLBAR),
}
}
const scrollEl = simulateScrollElement(400, 800, 16, 1)
console.log('Контейнер 400px, контент 800px:')
console.log(` offsetWidth = ${scrollEl.offsetWidth}px (с border)`)
console.log(` clientWidth = ${scrollEl.clientWidth}px (без border, без скроллбара)`)
console.log(` scrollWidth = ${scrollEl.scrollWidth}px (полный контент)`)
console.log(` Скроллируемый: ${scrollEl.canScroll}`)
// ===== overflow text-overflow =====
console.log('\n=== overflow паттерны ===')
// Симулируем text-overflow: ellipsis логикой
function truncateToWidth(text, maxChars, suffix = '…') {
if (text.length <= maxChars) return text
return text.slice(0, maxChars - suffix.length) + suffix
}
const longTitle = 'JavaScript — это высокоуровневый язык программирования'
const widths = [20, 30, 40, 50]
for (const w of widths) {
console.log(` ${w} chars: "${truncateToWidth(longTitle, w)}"`)
}
// ===== calc() паттерны =====
console.log('\n=== calc() в реальных ситуациях ===')
const VIEWPORT_HEIGHT = 900
const HEADER_H = 64, FOOTER_H = 48, PADDING = 32
const contentHeight = VIEWPORT_HEIGHT - HEADER_H - FOOTER_H - 2 * PADDING
console.log(`calc(100vh - 64px - 48px - 64px) = ${contentHeight}px — высота контента`)
console.log(`calc(50% - 8px) при 1200px = ${1200 / 2 - 8}px — колонка в 2-колоночном grid`)Ты ставишь элементу width: 200px, добавляешь padding: 20px — и он вдруг стал 240px. Или два блока с margin-bottom: 20px и margin-top: 30px — ожидаешь 50px между ними, а получаешь 30px. Или читаешь element.offsetWidth в JavaScript и получаешь не то, что думаешь. Box Model объясняет всё это.
Браузер вычисляет размеры элементов по Box Model. JavaScript-разработчик работает с этими размерами постоянно: позиционирование, анимации, вычисление доступного пространства. Непонимание box-sizing, margin collapse, и разницы offsetWidth/clientWidth приводит к багам в лейауте.
element.offsetWidth, clientWidth, getBoundingClientRect// content-box (исторический дефолт, ПЛОХОЙ):
// width: 200px + padding: 20px + border: 2px
// Итоговый offsetWidth = 200 + 40 + 4 = 244px (НЕОЖИДАННО!)
// border-box (современный стандарт, ХОРОШИЙ):
// width: 200px включает padding И border
// Итоговый offsetWidth = 200px (предсказуемо!)
// Контент = 200 - 40 - 4 = 156px
// Сброс на border-box (ставь всегда в начале CSS):
// *, *::before, *::after { box-sizing: border-box; }// Вертикальные margin соседних блоков схлопываются:
// .el1 { margin-bottom: 20px }
// .el2 { margin-top: 30px }
// Фактический отступ = max(20, 30) = 30px (не 50!)
// Схлопывание НЕ происходит:
// - в flex/grid контейнерах
// - при padding/border между родителем и первым/последним ребёнком
// - для inline-block элементов
// - горизонтальные margin НИКОГДА не схлопываются// visible — выходит за границы (по умолчанию)
// hidden — обрезается
// scroll — всегда показывает скроллбар
// auto — скроллбар только при необходимости (рекомендуется)
// clip — как hidden, но отключает прокрутку через JS
// overflow-x и overflow-y независимы:
// overflow-x: hidden; overflow-y: auto;
// Паттерны:
// Прокручиваемый список: max-height: 400px; overflow-y: auto;
// Обрезка текста: overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
// Скрытие для анимации: overflow: hidden; (+ transform)// offsetWidth = content + padding + border (без margin)
// clientWidth = content + padding (без border, без scrollbar)
// scrollWidth = полная ширина прокручиваемого контента
// getBoundingClientRect() — точные размеры с дробями:
// { width, height, top, left, right, bottom } — relative to viewport
// Наглядно:
// [margin | border | padding | content | padding | border | margin]
// ^-------- offsetWidth --------^
// ^--- clientWidth ---^
// ^content^
// scrollWidth >= clientWidth (если есть горизонтальный скролл)// Типичные паттерны:
// height: calc(100vh - 64px) — высота минус шапка
// width: calc(100% - 2 * 16px) — ширина минус отступы
// width: calc(50% - 8px) — в 2-колоночном grid с gap
// Из JavaScript:
element.style.width = `calc(100% - ${sidebarWidth}px)`Ошибка 1: Не использовать border-box
/* ПЛОХО — неожиданные размеры */
.input { width: 100%; padding: 12px 16px; }
/* Ширина стала 100% + 32px горизонтальных padding! */
/* ХОРОШО */
*, *::before, *::after { box-sizing: border-box; }
.input { width: 100%; padding: 12px 16px; }
/* Ширина = ровно 100%, padding внутри */Ошибка 2: Удивляться margin collapse
/* Ожидаешь 40px между параграфами, получаешь 20px */
p { margin-top: 20px; margin-bottom: 20px; }
/* Решение: */
p + p { margin-top: 0; }
/* или использовать padding вместо margin на контейнере */Ошибка 3: Путать offsetWidth и clientWidth
// Для позиционирования (включая border):
const fullWidth = element.offsetWidth
// Для внутреннего контента (без border, без scrollbar):
const innerWidth = element.clientWidth
// Для проверки переполнения:
const hasOverflow = element.scrollWidth > element.clientWidthbox-sizing: border-box + width: 100% — без сюрпризовmax-height + overflow-y: auto для выпадающих менюscrollHeight, scrollTop, clientHeight для расчёта видимых элементовoffsetWidth для вычисления размеров при перетаскиванииСимуляция Box Model: вычисление размеров, margin collapse, overflow и scrollable контейнеры
// Симуляция Box Model — чистые вычисления
// ===== Вычисление размеров =====
function calculateBoxModel({ width, height, padding, border, margin, scrollbarWidth = 0 }) {
// --- content-box (исторический) ---
const contentBox = {
contentWidth: width,
contentHeight: height,
offsetWidth: width + 2 * padding + 2 * border,
clientWidth: width + 2 * padding - scrollbarWidth,
totalWidth: width + 2 * padding + 2 * border + 2 * margin,
}
// --- border-box (современный) ---
const borderBox = {
contentWidth: width - 2 * padding - 2 * border,
contentHeight: height - 2 * padding - 2 * border,
offsetWidth: width, // width уже включает всё
clientWidth: width - 2 * border - scrollbarWidth,
totalWidth: width + 2 * margin,
}
return { contentBox, borderBox }
}
// === Демонстрация: content-box vs border-box ===
console.log('=== content-box vs border-box (width=200, padding=20, border=2, margin=16) ===')
const el = { width: 200, height: 100, padding: 20, border: 2, margin: 16 }
const { contentBox, borderBox } = calculateBoxModel(el)
console.log()
console.log('Свойство'.padEnd(18) + 'content-box'.padEnd(14) + 'border-box')
console.log('-'.repeat(44))
const props = ['contentWidth', 'offsetWidth', 'clientWidth', 'totalWidth']
for (const p of props) {
console.log(p.padEnd(18) + String(contentBox[p]).padEnd(14) + String(borderBox[p]))
}
console.log()
console.log('content-box: ожидаешь 200px, получаешь offsetWidth =', contentBox.offsetWidth, '(НЕОЖИДАННО!)')
console.log('border-box: width =', borderBox.offsetWidth, '(ПРЕДСКАЗУЕМО!)')
// ===== Margin Collapse =====
console.log('\n=== Margin Collapse ===')
function verticalGap(mb, mt, isFlex = false) {
// В flex/grid — отступы СКЛАДЫВАЮТСЯ (нет collapse)
return isFlex ? mb + mt : Math.max(mb, mt)
}
const scenarios = [
{ mb: 20, mt: 20, flex: false, desc: 'div(mb:20) + div(mt:20) — обычный поток' },
{ mb: 20, mt: 30, flex: false, desc: 'div(mb:20) + div(mt:30) — обычный поток' },
{ mb: 20, mt: 30, flex: true, desc: 'flex-item(mb:20) + flex-item(mt:30)' },
{ mb: 0, mt: 40, flex: false, desc: 'div(mb:0) + div(mt:40)' },
]
for (const s of scenarios) {
const gap = verticalGap(s.mb, s.mt, s.flex)
const note = s.flex ? '(flex — нет collapse)' : '(collapse: max)'
console.log(`${s.desc}`)
console.log(` ${s.mb}px + ${s.mt}px = ${gap}px ${note}`)
}
// ===== offsetWidth vs clientWidth vs scrollWidth =====
console.log('\n=== offsetWidth / clientWidth / scrollWidth ===')
function simulateScrollElement(containerW, contentW, padding, border) {
// clientWidth: ширина без border, без скроллбара (15px стандарт)
const SCROLLBAR = 15
return {
offsetWidth: containerW,
clientWidth: containerW - 2 * border - SCROLLBAR,
scrollWidth: contentW + 2 * padding,
canScroll: (contentW + 2 * padding) > (containerW - 2 * border - SCROLLBAR),
}
}
const scrollEl = simulateScrollElement(400, 800, 16, 1)
console.log('Контейнер 400px, контент 800px:')
console.log(` offsetWidth = ${scrollEl.offsetWidth}px (с border)`)
console.log(` clientWidth = ${scrollEl.clientWidth}px (без border, без скроллбара)`)
console.log(` scrollWidth = ${scrollEl.scrollWidth}px (полный контент)`)
console.log(` Скроллируемый: ${scrollEl.canScroll}`)
// ===== overflow text-overflow =====
console.log('\n=== overflow паттерны ===')
// Симулируем text-overflow: ellipsis логикой
function truncateToWidth(text, maxChars, suffix = '…') {
if (text.length <= maxChars) return text
return text.slice(0, maxChars - suffix.length) + suffix
}
const longTitle = 'JavaScript — это высокоуровневый язык программирования'
const widths = [20, 30, 40, 50]
for (const w of widths) {
console.log(` ${w} chars: "${truncateToWidth(longTitle, w)}"`)
}
// ===== calc() паттерны =====
console.log('\n=== calc() в реальных ситуациях ===')
const VIEWPORT_HEIGHT = 900
const HEADER_H = 64, FOOTER_H = 48, PADDING = 32
const contentHeight = VIEWPORT_HEIGHT - HEADER_H - FOOTER_H - 2 * PADDING
console.log(`calc(100vh - 64px - 48px - 64px) = ${contentHeight}px — высота контента`)
console.log(`calc(50% - 8px) при 1200px = ${1200 / 2 - 8}px — колонка в 2-колоночном grid`)Реализуй функцию `calculateBoxSizing(params)` которая вычисляет ключевые размеры в обоих режимах `box-sizing`. `params`: `{ width, padding, border, margin, scrollbarWidth = 0 }` Возвращает: `{ contentBox, borderBox }` — каждый с полями `contentWidth`, `offsetWidth`, `clientWidth`, `totalWidth`
content-box offsetWidth = width + 2*padding + 2*border. content-box clientWidth = width + 2*padding - scrollbarWidth. border-box contentWidth = width - 2*padding - 2*border. border-box clientWidth = width - 2*border - scrollbarWidth