Ты пишешь дропдаун-меню: оно должно появляться точно под кнопкой и не смещать остальной контент. Или строишь дашборд: sidebar фиксированный, основной контент заполняет остаток. Или позиционируешь тултип так, чтобы он не вылезал за края экрана. Всё это — display, position, Flexbox. Ты управляешь ими из JavaScript каждый день.
JavaScript-разработчик постоянно читает и изменяет CSS-раскладку: прячет/показывает элементы, позиционирует тултипы и дропдауны, строит адаптивные лейауты. Без понимания как работает display/position/flex — код будет полон магических констант и непредсказуемых багов.
element.style.display, element.getBoundingClientRect()offsetWidth, clientWidth зависят от display/position// block — занимает всю ширину строки, новая строка до и после
// div, p, section, h1-h6, ul, li — block по умолчанию
// inline — ширина по содержимому, width/height игнорируются
// span, a, strong, em — inline по умолчанию
// inline-block — как inline (в потоке), но принимает width/height
// flex — контейнер Flexbox для дочерних элементов
// grid — контейнер CSS Grid
// none — элемент скрыт и не занимает место// Три способа скрыть элемент — разные последствия:
// 1. display: none — убирает из потока документа полностью
element.style.display = 'none'
element.style.display = '' // убрать инлайн-стиль (восстановить)
// 2. visibility: hidden — скрыт, но место СОХРАНЯЕТСЯ
element.style.visibility = 'hidden'
// 3. opacity: 0 — прозрачен, место сохраняется, СОБЫТИЯ работают!
element.style.opacity = '0'
// Читаем:
const display = getComputedStyle(element).display // 'block', 'flex', etc.// static — обычный поток (по умолчанию)
// top/left/right/bottom НЕ работают
// relative — смещается от своего нормального места
// Создаёт контекст позиционирования для absolute-потомков
// Исходное место в потоке сохраняется
// absolute — вырван из потока
// Позиционируется относительно ближайшего non-static предка
// Если такого нет — от <html>
// fixed — позиционируется от viewport
// Не сдвигается при прокрутке (навбары, кнопка "наверх")
// sticky — гибрид relative + fixed
// relative пока не достигнет порогового значения
// Потом "прилипает" (заголовки таблиц, sticky nav)
// z-index работает только для positioned элементов (не static)// На контейнере (display: flex):
// flex-direction: row | column | row-reverse | column-reverse
// justify-content: flex-start | center | flex-end | space-between | space-around | space-evenly
// align-items: stretch | flex-start | center | flex-end | baseline
// gap: 16px (или row-gap + column-gap отдельно)
// flex-wrap: nowrap | wrap
// На дочерних (flex items):
// flex: 1 → flex: 1 1 0 (равномерно растягивается)
// flex: auto → flex: 1 1 auto
// flex-grow: 2 → растягивается в 2 раза больше, чем flex-grow: 1
// flex-shrink: 0 → не сжимается
// flex-basis: 200px → начальный размер
// align-self: auto | flex-start | center | flex-end | stretchfunction positionTooltip(anchor, tooltip) {
const rect = anchor.getBoundingClientRect()
const scrollY = window.scrollY
const scrollX = window.scrollX
// Размещаем снизу от anchor
let top = rect.bottom + scrollY + 8
let left = rect.left + scrollX + (rect.width - tooltip.offsetWidth) / 2
// Не даём выйти за правый край
const maxLeft = document.documentElement.clientWidth - tooltip.offsetWidth - 8
if (left > maxLeft) left = maxLeft
tooltip.style.position = 'absolute'
tooltip.style.top = top + 'px'
tooltip.style.left = left + 'px'
}Ошибка 1: Абсолютное позиционирование без relative-родителя
// ПЛОХО — дропдаун позиционируется от <html>, а не от кнопки
// <div class="btn"><div class="dropdown" style="position:absolute"></div></div>
// Кнопка без position: relative!
// ВЕРНО
// .btn { position: relative; }
// .dropdown { position: absolute; top: 100%; left: 0; }Ошибка 2: z-index без positioned-контекста
// НЕВЕРНО — z-index не работает на static элементах
element.style.zIndex = '999' // Нет эффекта, если position: static
// ВЕРНО
element.style.position = 'relative'
element.style.zIndex = '999'Ошибка 3: display:none vs visibility:hidden для анимаций
// display:none невозможно анимировать transition
// НЕВЕРНО — transition не работает
element.style.transition = 'opacity 0.3s'
element.style.display = 'none'
// ВЕРНО — комбинируем opacity + pointer-events
element.style.opacity = '0'
element.style.pointerEvents = 'none'
// Потом:
element.style.opacity = '1'
element.style.pointerEvents = 'auto'position: absolute внутри position: relative-контейнераposition: fixed + overlayposition: sticky; top: 0 + z-indexalign-items: stretchСимуляция flex-раскладки и позиционирования тултипов: вычисления без DOM
// Симуляция CSS-раскладки — чистые вычисления
// ===== Flexbox: распределение пространства =====
function calculateFlexLayout(containerWidth, items, options = {}) {
const { gap = 0, padding = 0 } = options
// Доступная ширина: контейнер - паддинги - промежутки
const available = containerWidth - 2 * padding - gap * (items.length - 1)
// Разделяем фиксированные и flex-элементы
const fixedItems = items.filter(item => item.width !== undefined && !item.flexGrow)
const flexItems = items.filter(item => item.flexGrow !== undefined)
const fixedTotal = fixedItems.reduce((sum, item) => sum + (item.width ?? 0), 0)
const flexSpace = available - fixedTotal
const totalFlex = flexItems.reduce((sum, item) => sum + (item.flexGrow ?? 1), 0)
return items.map(item => {
const width = item.flexGrow !== undefined
? Math.round((item.flexGrow / totalFlex) * flexSpace)
: (item.width ?? 0)
return { name: item.name, width }
})
}
function printLayout(items, containerWidth, title) {
console.log(`${title} (container: ${containerWidth}px)`)
const totalWidth = items.reduce((s, i) => s + i.width, 0)
for (const item of items) {
const bar = '█'.repeat(Math.round(item.width / 20))
const pct = Math.round(item.width / containerWidth * 100)
console.log(` ${item.name.padEnd(14)}: ${String(item.width).padStart(4)}px (${String(pct).padStart(3)}%) ${bar}`)
}
console.log(` Итого: ${totalWidth}px / ${containerWidth}px`)
}
// === Равномерное распределение (flex: 1) ===
console.log('=== Flex раскладки ===')
const equal = calculateFlexLayout(1200, [
{ name: 'Col 1', flexGrow: 1 },
{ name: 'Col 2', flexGrow: 1 },
{ name: 'Col 3', flexGrow: 1 },
])
printLayout(equal, 1200, 'flex:1 для всех')
// === Sidebar (1) + Content (3) ===
const sidebar = calculateFlexLayout(1200, [
{ name: 'Sidebar', flexGrow: 1 },
{ name: 'Content', flexGrow: 3 },
], { gap: 24 })
printLayout(sidebar, 1200 - 24, 'Sidebar(1) + Content(3)')
// === Fixed sidebar + Flexible content + Fixed panel ===
console.log()
const mixed = calculateFlexLayout(1200, [
{ name: 'Fixed Nav', width: 240 },
{ name: 'Main', flexGrow: 1 },
{ name: 'Fixed Panel', width: 320 },
], { gap: 16 })
printLayout(mixed, 1200, 'Fixed(240) + Flex(1) + Fixed(320)')
// ===== position: absolute — тултип =====
console.log('\n=== Позиционирование тултипа ===')
function positionTooltip(anchorRect, tooltipSize, viewport) {
const { top, left, bottom, width } = anchorRect
const { tooltipWidth, tooltipHeight } = tooltipSize
const { vpWidth, vpHeight, scrollY = 0 } = viewport
// Пробуем снизу от anchor
let tipTop = bottom + scrollY + 8
let tipLeft = left + (width - tooltipWidth) / 2
// Не выходим за правый край
tipLeft = Math.min(tipLeft, vpWidth - tooltipWidth - 8)
tipLeft = Math.max(tipLeft, 8)
// Не выходим за нижний край — ставим сверху
let placement = 'bottom'
if (bottom + tooltipHeight + 8 > vpHeight) {
tipTop = top + scrollY - tooltipHeight - 8
placement = 'top'
}
return {
top: Math.round(tipTop),
left: Math.round(tipLeft),
placement,
}
}
const scenarios = [
{
label: 'Кнопка посередине экрана',
anchor: { top: 400, left: 600, bottom: 430, width: 120, height: 30 },
tooltip: { tooltipWidth: 200, tooltipHeight: 40 },
vp: { vpWidth: 1440, vpHeight: 900, scrollY: 0 },
},
{
label: 'Кнопка у нижнего края — тултип переходит наверх',
anchor: { top: 860, left: 200, bottom: 890, width: 120, height: 30 },
tooltip: { tooltipWidth: 200, tooltipHeight: 40 },
vp: { vpWidth: 1440, vpHeight: 900, scrollY: 0 },
},
{
label: 'Кнопка у правого края — тултип не выходит за экран',
anchor: { top: 400, left: 1380, bottom: 430, width: 50, height: 30 },
tooltip: { tooltipWidth: 200, tooltipHeight: 40 },
vp: { vpWidth: 1440, vpHeight: 900, scrollY: 0 },
},
]
for (const { label, anchor, tooltip, vp } of scenarios) {
const pos = positionTooltip(anchor, tooltip, vp)
console.log(`${label}:`)
console.log(` top=${pos.top} left=${pos.left} placement=${pos.placement}`)
}Ты пишешь дропдаун-меню: оно должно появляться точно под кнопкой и не смещать остальной контент. Или строишь дашборд: sidebar фиксированный, основной контент заполняет остаток. Или позиционируешь тултип так, чтобы он не вылезал за края экрана. Всё это — display, position, Flexbox. Ты управляешь ими из JavaScript каждый день.
JavaScript-разработчик постоянно читает и изменяет CSS-раскладку: прячет/показывает элементы, позиционирует тултипы и дропдауны, строит адаптивные лейауты. Без понимания как работает display/position/flex — код будет полон магических констант и непредсказуемых багов.
element.style.display, element.getBoundingClientRect()offsetWidth, clientWidth зависят от display/position// block — занимает всю ширину строки, новая строка до и после
// div, p, section, h1-h6, ul, li — block по умолчанию
// inline — ширина по содержимому, width/height игнорируются
// span, a, strong, em — inline по умолчанию
// inline-block — как inline (в потоке), но принимает width/height
// flex — контейнер Flexbox для дочерних элементов
// grid — контейнер CSS Grid
// none — элемент скрыт и не занимает место// Три способа скрыть элемент — разные последствия:
// 1. display: none — убирает из потока документа полностью
element.style.display = 'none'
element.style.display = '' // убрать инлайн-стиль (восстановить)
// 2. visibility: hidden — скрыт, но место СОХРАНЯЕТСЯ
element.style.visibility = 'hidden'
// 3. opacity: 0 — прозрачен, место сохраняется, СОБЫТИЯ работают!
element.style.opacity = '0'
// Читаем:
const display = getComputedStyle(element).display // 'block', 'flex', etc.// static — обычный поток (по умолчанию)
// top/left/right/bottom НЕ работают
// relative — смещается от своего нормального места
// Создаёт контекст позиционирования для absolute-потомков
// Исходное место в потоке сохраняется
// absolute — вырван из потока
// Позиционируется относительно ближайшего non-static предка
// Если такого нет — от <html>
// fixed — позиционируется от viewport
// Не сдвигается при прокрутке (навбары, кнопка "наверх")
// sticky — гибрид relative + fixed
// relative пока не достигнет порогового значения
// Потом "прилипает" (заголовки таблиц, sticky nav)
// z-index работает только для positioned элементов (не static)// На контейнере (display: flex):
// flex-direction: row | column | row-reverse | column-reverse
// justify-content: flex-start | center | flex-end | space-between | space-around | space-evenly
// align-items: stretch | flex-start | center | flex-end | baseline
// gap: 16px (или row-gap + column-gap отдельно)
// flex-wrap: nowrap | wrap
// На дочерних (flex items):
// flex: 1 → flex: 1 1 0 (равномерно растягивается)
// flex: auto → flex: 1 1 auto
// flex-grow: 2 → растягивается в 2 раза больше, чем flex-grow: 1
// flex-shrink: 0 → не сжимается
// flex-basis: 200px → начальный размер
// align-self: auto | flex-start | center | flex-end | stretchfunction positionTooltip(anchor, tooltip) {
const rect = anchor.getBoundingClientRect()
const scrollY = window.scrollY
const scrollX = window.scrollX
// Размещаем снизу от anchor
let top = rect.bottom + scrollY + 8
let left = rect.left + scrollX + (rect.width - tooltip.offsetWidth) / 2
// Не даём выйти за правый край
const maxLeft = document.documentElement.clientWidth - tooltip.offsetWidth - 8
if (left > maxLeft) left = maxLeft
tooltip.style.position = 'absolute'
tooltip.style.top = top + 'px'
tooltip.style.left = left + 'px'
}Ошибка 1: Абсолютное позиционирование без relative-родителя
// ПЛОХО — дропдаун позиционируется от <html>, а не от кнопки
// <div class="btn"><div class="dropdown" style="position:absolute"></div></div>
// Кнопка без position: relative!
// ВЕРНО
// .btn { position: relative; }
// .dropdown { position: absolute; top: 100%; left: 0; }Ошибка 2: z-index без positioned-контекста
// НЕВЕРНО — z-index не работает на static элементах
element.style.zIndex = '999' // Нет эффекта, если position: static
// ВЕРНО
element.style.position = 'relative'
element.style.zIndex = '999'Ошибка 3: display:none vs visibility:hidden для анимаций
// display:none невозможно анимировать transition
// НЕВЕРНО — transition не работает
element.style.transition = 'opacity 0.3s'
element.style.display = 'none'
// ВЕРНО — комбинируем opacity + pointer-events
element.style.opacity = '0'
element.style.pointerEvents = 'none'
// Потом:
element.style.opacity = '1'
element.style.pointerEvents = 'auto'position: absolute внутри position: relative-контейнераposition: fixed + overlayposition: sticky; top: 0 + z-indexalign-items: stretchСимуляция flex-раскладки и позиционирования тултипов: вычисления без DOM
// Симуляция CSS-раскладки — чистые вычисления
// ===== Flexbox: распределение пространства =====
function calculateFlexLayout(containerWidth, items, options = {}) {
const { gap = 0, padding = 0 } = options
// Доступная ширина: контейнер - паддинги - промежутки
const available = containerWidth - 2 * padding - gap * (items.length - 1)
// Разделяем фиксированные и flex-элементы
const fixedItems = items.filter(item => item.width !== undefined && !item.flexGrow)
const flexItems = items.filter(item => item.flexGrow !== undefined)
const fixedTotal = fixedItems.reduce((sum, item) => sum + (item.width ?? 0), 0)
const flexSpace = available - fixedTotal
const totalFlex = flexItems.reduce((sum, item) => sum + (item.flexGrow ?? 1), 0)
return items.map(item => {
const width = item.flexGrow !== undefined
? Math.round((item.flexGrow / totalFlex) * flexSpace)
: (item.width ?? 0)
return { name: item.name, width }
})
}
function printLayout(items, containerWidth, title) {
console.log(`${title} (container: ${containerWidth}px)`)
const totalWidth = items.reduce((s, i) => s + i.width, 0)
for (const item of items) {
const bar = '█'.repeat(Math.round(item.width / 20))
const pct = Math.round(item.width / containerWidth * 100)
console.log(` ${item.name.padEnd(14)}: ${String(item.width).padStart(4)}px (${String(pct).padStart(3)}%) ${bar}`)
}
console.log(` Итого: ${totalWidth}px / ${containerWidth}px`)
}
// === Равномерное распределение (flex: 1) ===
console.log('=== Flex раскладки ===')
const equal = calculateFlexLayout(1200, [
{ name: 'Col 1', flexGrow: 1 },
{ name: 'Col 2', flexGrow: 1 },
{ name: 'Col 3', flexGrow: 1 },
])
printLayout(equal, 1200, 'flex:1 для всех')
// === Sidebar (1) + Content (3) ===
const sidebar = calculateFlexLayout(1200, [
{ name: 'Sidebar', flexGrow: 1 },
{ name: 'Content', flexGrow: 3 },
], { gap: 24 })
printLayout(sidebar, 1200 - 24, 'Sidebar(1) + Content(3)')
// === Fixed sidebar + Flexible content + Fixed panel ===
console.log()
const mixed = calculateFlexLayout(1200, [
{ name: 'Fixed Nav', width: 240 },
{ name: 'Main', flexGrow: 1 },
{ name: 'Fixed Panel', width: 320 },
], { gap: 16 })
printLayout(mixed, 1200, 'Fixed(240) + Flex(1) + Fixed(320)')
// ===== position: absolute — тултип =====
console.log('\n=== Позиционирование тултипа ===')
function positionTooltip(anchorRect, tooltipSize, viewport) {
const { top, left, bottom, width } = anchorRect
const { tooltipWidth, tooltipHeight } = tooltipSize
const { vpWidth, vpHeight, scrollY = 0 } = viewport
// Пробуем снизу от anchor
let tipTop = bottom + scrollY + 8
let tipLeft = left + (width - tooltipWidth) / 2
// Не выходим за правый край
tipLeft = Math.min(tipLeft, vpWidth - tooltipWidth - 8)
tipLeft = Math.max(tipLeft, 8)
// Не выходим за нижний край — ставим сверху
let placement = 'bottom'
if (bottom + tooltipHeight + 8 > vpHeight) {
tipTop = top + scrollY - tooltipHeight - 8
placement = 'top'
}
return {
top: Math.round(tipTop),
left: Math.round(tipLeft),
placement,
}
}
const scenarios = [
{
label: 'Кнопка посередине экрана',
anchor: { top: 400, left: 600, bottom: 430, width: 120, height: 30 },
tooltip: { tooltipWidth: 200, tooltipHeight: 40 },
vp: { vpWidth: 1440, vpHeight: 900, scrollY: 0 },
},
{
label: 'Кнопка у нижнего края — тултип переходит наверх',
anchor: { top: 860, left: 200, bottom: 890, width: 120, height: 30 },
tooltip: { tooltipWidth: 200, tooltipHeight: 40 },
vp: { vpWidth: 1440, vpHeight: 900, scrollY: 0 },
},
{
label: 'Кнопка у правого края — тултип не выходит за экран',
anchor: { top: 400, left: 1380, bottom: 430, width: 50, height: 30 },
tooltip: { tooltipWidth: 200, tooltipHeight: 40 },
vp: { vpWidth: 1440, vpHeight: 900, scrollY: 0 },
},
]
for (const { label, anchor, tooltip, vp } of scenarios) {
const pos = positionTooltip(anchor, tooltip, vp)
console.log(`${label}:`)
console.log(` top=${pos.top} left=${pos.left} placement=${pos.placement}`)
}Реализуй функцию `calculateFlexLayout(containerWidth, items, gap)` которая вычисляет ширину flex-элементов. Все элементы имеют `flexGrow` (число). Пространство распределяется пропорционально `flexGrow` после вычета промежутков (`gap` между элементами).
totalGap = gap * (items.length - 1). available = containerWidth - totalGap. totalFlex = items.reduce((s,i) => s + i.flexGrow, 0). width = Math.round((item.flexGrow / totalFlex) * available)