← JavaScript/display, position и Flexbox#168 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

display, position и Flexbox

Ты пишешь дропдаун-меню: оно должно появляться точно под кнопкой и не смещать остальной контент. Или строишь дашборд: sidebar фиксированный, основной контент заполняет остаток. Или позиционируешь тултип так, чтобы он не вылезал за края экрана. Всё это — display, position, Flexbox. Ты управляешь ими из JavaScript каждый день.

Какую проблему решает

JavaScript-разработчик постоянно читает и изменяет CSS-раскладку: прячет/показывает элементы, позиционирует тултипы и дропдауны, строит адаптивные лейауты. Без понимания как работает display/position/flex — код будет полон магических констант и непредсказуемых багов.

На основе предыдущих уроков

  • DOM: element.style.display, element.getBoundingClientRect()
  • CSS units: позиционирование использует px, %, rem
  • Box Model: offsetWidth, clientWidth зависят от display/position
  • display — тип блока

    // 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 — элемент скрыт и не занимает место

    Управление отображением из JavaScript

    // Три способа скрыть элемент — разные последствия:
    
    // 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.

    position — система позиционирования

    // static — обычный поток (по умолчанию)
    //   top/left/right/bottom НЕ работают
    
    // relative — смещается от своего нормального места
    //   Создаёт контекст позиционирования для absolute-потомков
    //   Исходное место в потоке сохраняется
    
    // absolute — вырван из потока
    //   Позиционируется относительно ближайшего non-static предка
    //   Если такого нет — от <html>
    
    // fixed — позиционируется от viewport
    //   Не сдвигается при прокрутке (навбары, кнопка "наверх")
    
    // sticky — гибрид relative + fixed
    //   relative пока не достигнет порогового значения
    //   Потом "прилипает" (заголовки таблиц, sticky nav)
    
    // z-index работает только для positioned элементов (не static)

    Flexbox — гибкая раскладка

    // На контейнере (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 | stretch

    Позиционирование тултипов из JavaScript

    function 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 + overlay
  • Sticky header: position: sticky; top: 0 + z-index
  • Dashboard layout: flex-контейнер с sidebar (фиксированный) + main (flex: 1)
  • Карточки одинаковой высоты: flex на контейнере, align-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}`)
    }

    display, position и Flexbox

    Ты пишешь дропдаун-меню: оно должно появляться точно под кнопкой и не смещать остальной контент. Или строишь дашборд: sidebar фиксированный, основной контент заполняет остаток. Или позиционируешь тултип так, чтобы он не вылезал за края экрана. Всё это — display, position, Flexbox. Ты управляешь ими из JavaScript каждый день.

    Какую проблему решает

    JavaScript-разработчик постоянно читает и изменяет CSS-раскладку: прячет/показывает элементы, позиционирует тултипы и дропдауны, строит адаптивные лейауты. Без понимания как работает display/position/flex — код будет полон магических констант и непредсказуемых багов.

    На основе предыдущих уроков

  • DOM: element.style.display, element.getBoundingClientRect()
  • CSS units: позиционирование использует px, %, rem
  • Box Model: offsetWidth, clientWidth зависят от display/position
  • display — тип блока

    // 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 — элемент скрыт и не занимает место

    Управление отображением из JavaScript

    // Три способа скрыть элемент — разные последствия:
    
    // 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.

    position — система позиционирования

    // static — обычный поток (по умолчанию)
    //   top/left/right/bottom НЕ работают
    
    // relative — смещается от своего нормального места
    //   Создаёт контекст позиционирования для absolute-потомков
    //   Исходное место в потоке сохраняется
    
    // absolute — вырван из потока
    //   Позиционируется относительно ближайшего non-static предка
    //   Если такого нет — от <html>
    
    // fixed — позиционируется от viewport
    //   Не сдвигается при прокрутке (навбары, кнопка "наверх")
    
    // sticky — гибрид relative + fixed
    //   relative пока не достигнет порогового значения
    //   Потом "прилипает" (заголовки таблиц, sticky nav)
    
    // z-index работает только для positioned элементов (не static)

    Flexbox — гибкая раскладка

    // На контейнере (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 | stretch

    Позиционирование тултипов из JavaScript

    function 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 + overlay
  • Sticky header: position: sticky; top: 0 + z-index
  • Dashboard layout: flex-контейнер с sidebar (фиксированный) + main (flex: 1)
  • Карточки одинаковой высоты: flex на контейнере, align-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)

    Загружаем среду выполнения...
    Загружаем AI-помощника...