← JavaScript/Единицы CSS для JS-разработчика#167 из 383← ПредыдущийСледующий →+15 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Единицы CSS для JS-разработчика

Ты получаешь из дизайна размеры в px, а пользователь увеличил шрифт в браузере до 20px — весь интерфейс сломался. Или добавляешь em для отступов внутри компонента, вкладываешь его в другой компонент — и шрифт становится 48px вместо 12px. Понимание единиц CSS спасает от этих ловушек.

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

Разные единицы предназначены для разных задач. Неправильный выбор ведёт к неадаптивному интерфейсу, проблемам с доступностью (пользователи с большим шрифтом), и непредсказуемому поведению в разных контекстах.

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

  • CSS свойства из JS: element.style.width — как устанавливать и читать
  • Flexbox: % для fluid layout в flex-контейнерах
  • Box Model: padding в em масштабируется вместе с шрифтом
  • Абсолютные единицы

    // px — пиксели (основная единица)
    // На Retina: 1 CSS px = 2 физических пикселя (devicePixelRatio = 2)
    // Используй для: border, box-shadow, border-radius, медиа-запросы
    
    // pt — типографская единица. 1pt = 1/72 дюйма ≈ 1.33px
    // Только для печати (print stylesheet)

    Относительные единицы

    // rem — от root font-size (html элемент, обычно 16px)
    // Предсказуем, не накапливается при вложении
    // Используй для: font-size, padding, margin, max-width
    
    // em — от ТЕКУЩЕГО font-size элемента
    // Накапливается при вложении — опасность!
    // Используй для: padding внутри кнопок (масштабируется со шрифтом кнопки)
    
    // % — от родительского элемента
    // width: 50% = половина родителя
    // padding-top: 50% = 50% от ШИРИНЫ (!) родителя — ловушка!
    
    // vh — 1% высоты viewport
    // vw — 1% ширины viewport
    // dvh — dynamic viewport height (учитывает мобильный браузер)
    // vmin — min(vw, vh), vmax — max(vw, vh)

    Когда что использовать

    | Единица | Лучше для |

    |---------|-----------|

    | rem | font-size, spacing, max-width компонентов |

    | px | border, shadow, border-radius, breakpoints |

    | % | width в flex/grid, fluid layout |

    | em | padding кнопок (относительно их шрифта) |

    | vh/vw | hero-секции, полноэкранные модалы |

    Ловушка: em-нестование

    // html: 16px (корень)
    // div { font-size: 1.5em }  → 24px
    // span { font-size: 1.5em } → 36px (1.5 × 24!)
    // p    { font-size: 1.5em } → 54px (1.5 × 36!)
    
    // rem не накапливается:
    // div { font-size: 1.5rem } → 24px (всегда от root)
    // span { font-size: 1.5rem } → 24px (независимо от родителя!)

    CSS calc() — комбинирование единиц

    // Комбинирование разных единиц — только так возможно
    // calc(100% - 40px)    — ширина минус отступы
    // calc(100vh - 64px)   — высота viewport минус шапка
    // calc(1rem + 2vw)     — адаптивный шрифт (Fluid Typography)
    // calc(50% - 160px)    — позиционирование по центру
    
    // Из JavaScript:
    // element.style.width = `calc(100% - ${sidebarWidth}px)`

    getComputedStyle в JavaScript

    // getComputedStyle ВСЕГДА возвращает вычисленные px-значения!
    const style = getComputedStyle(element)
    style.fontSize  // '16px'  — даже если задан 1rem
    style.width     // '320px' — даже если задан 20%
    
    // Конвертация px → rem:
    const pxSize  = parseFloat(style.fontSize)  // 16
    const rootPx  = parseFloat(getComputedStyle(document.documentElement).fontSize)
    const remSize = pxSize / rootPx  // 1

    Типичные ошибки

    Ошибка 1: px для font-size

    /* ПЛОХО — игнорирует настройки браузера пользователя */
    body { font-size: 16px; }
    
    /* ХОРОШО — масштабируется с браузером */
    body { font-size: 1rem; }

    Ошибка 2: em для font-size в компонентах

    /* ПЛОХО — накапливается при вложении */
    .card { font-size: 0.875em; }
    .card .inner { font-size: 0.875em; } /* → 0.875 × 0.875 = 0.766rem! */
    
    /* ХОРОШО — всегда предсказуемо */
    .card { font-size: 0.875rem; }
    .card .inner { font-size: 0.875rem; }

    Ошибка 3: vh без учёта мобильных браузеров

    /* ПЛОХО — на iOS 100vh включает адресную строку → обрезается */
    .hero { height: 100vh; }
    
    /* ХОРОШО — dvh учитывает динамический viewport */
    .hero { height: 100dvh; }
    /* или fallback: */
    .hero { height: 100vh; height: 100dvh; }

    В реальных проектах

  • Дизайн-системы: spacing scale в rem (4px = 0.25rem, 8px = 0.5rem, 16px = 1rem)
  • Fluid Typography: font-size: clamp(1rem, 1rem + 2vw, 1.5rem) — автоматически адаптируется
  • Полноэкранные секции: height: 100dvh для hero и модальных окон
  • JS-анимации: конвертация единиц для точных расчётов позиций
  • Примеры

    Конвертер CSS единиц: rem, em, px, vh, vw — вычисления и ловушки em-нестования

    // Конвертер CSS единиц — чистые вычисления без DOM
    
    // Конфигурация среды
    const env = {
      rootFontSize:   16,    // px (html font-size)
      viewportWidth:  1440,  // px
      viewportHeight: 900,   // px
      devicePixelRatio: 2,   // Retina
    }
    
    // ===== Конвертеры =====
    
    const convert = {
      remToPx: (rem, root = env.rootFontSize)  => rem * root,
      pxToRem: (px, root = env.rootFontSize)   => px / root,
      emToPx:  (em, parentSize)               => em * parentSize,
      vhToPx:  (vh, vh100 = env.viewportHeight) => (vh / 100) * vh100,
      vwToPx:  (vw, vw100 = env.viewportWidth)  => (vw / 100) * vw100,
      pctToPx: (pct, parentPx)               => (pct / 100) * parentPx,
    }
    
    // ===== Демонстрация =====
    
    console.log('=== rem ↔ px (root = 16px) ===')
    const remValues = [0.5, 0.75, 0.875, 1, 1.125, 1.25, 1.5, 2, 3]
    for (const rem of remValues) {
      console.log(`  ${String(rem).padEnd(6)}rem = ${convert.remToPx(rem)}px`)
    }
    
    console.log('\n=== px → rem ===')
    const pxValues = [8, 12, 14, 16, 18, 20, 24, 32, 48, 64]
    for (const px of pxValues) {
      const rem = convert.pxToRem(px)
      console.log(`  ${String(px).padStart(3)}px = ${rem.toFixed(4)}rem`)
    }
    
    // ===== EM: ловушка нестования =====
    console.log('\n=== em — ловушка нестования ===')
    const rootFont = 16
    
    let parentFont  = convert.emToPx(1.5, rootFont)    // html → div
    let childFont   = convert.emToPx(1.5, parentFont)  // div → span
    let grandChild  = convert.emToPx(1.5, childFont)   // span → p
    
    console.log(`  html  (root):     ${rootFont}px`)
    console.log(`  div   (1.5em):    ${parentFont}px   (×1.5 от root)`)
    console.log(`  span  (1.5em):    ${childFont}px   (×2.25 от root!)`)
    console.log(`  p     (1.5em):    ${grandChild}px  (×3.375 от root!!)`)
    console.log()
    console.log('  rem НЕ накапливается:')
    console.log(`  div   (1.5rem):   ${convert.remToPx(1.5)}px  (всегда от root)`)
    console.log(`  span  (1.5rem):   ${convert.remToPx(1.5)}px  (не зависит от родителя)`)
    
    // ===== vh/vw =====
    console.log(`\n=== vh/vw (viewport ${env.viewportWidth}×${env.viewportHeight}) ===`)
    const vhValues = [10, 25, 50, 100]
    const vwValues = [10, 25, 33, 50, 100]
    
    console.log('vh:')
    for (const vh of vhValues) {
      console.log(`  ${String(vh).padStart(4)}vh = ${convert.vhToPx(vh)}px`)
    }
    
    console.log('vw:')
    for (const vw of vwValues) {
      console.log(`  ${String(vw).padStart(4)}vw = ${convert.vwToPx(vw)}px`)
    }
    
    // ===== Типовая spacing scale (Tailwind/дизайн-системы) =====
    console.log('\n=== Spacing scale (base = 4px = 0.25rem) ===')
    const spacingScale = [
      [0, 0],    [1, 4],   [2, 8],   [3, 12],  [4, 16],
      [5, 20],   [6, 24],  [8, 32],  [10, 40], [12, 48],
      [16, 64],  [20, 80], [24, 96],
    ]
    for (const [name, px] of spacingScale) {
      const rem = convert.pxToRem(px)
      console.log(`  spacing-${String(name).padEnd(3)}: ${String(px).padStart(3)}px = ${rem.toFixed(4)}rem`)
    }
    
    // ===== calc() симуляция =====
    console.log('\n=== calc() применение ===')
    const examples = [
      { expr: '100vw - 2*24px',         result: convert.vwToPx(100) - 2 * 24 },
      { expr: '100vh - 64px',           result: convert.vhToPx(100) - 64 },
      { expr: '50% - 1rem (parent 1200px)', result: convert.pctToPx(50, 1200) - convert.remToPx(1) },
      { expr: '1rem + 2vw',             result: convert.remToPx(1) + convert.vwToPx(2) },
    ]
    for (const { expr, result } of examples) {
      console.log(`  calc(${expr}) = ${Math.round(result)}px`)
    }

    Единицы CSS для JS-разработчика

    Ты получаешь из дизайна размеры в px, а пользователь увеличил шрифт в браузере до 20px — весь интерфейс сломался. Или добавляешь em для отступов внутри компонента, вкладываешь его в другой компонент — и шрифт становится 48px вместо 12px. Понимание единиц CSS спасает от этих ловушек.

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

    Разные единицы предназначены для разных задач. Неправильный выбор ведёт к неадаптивному интерфейсу, проблемам с доступностью (пользователи с большим шрифтом), и непредсказуемому поведению в разных контекстах.

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

  • CSS свойства из JS: element.style.width — как устанавливать и читать
  • Flexbox: % для fluid layout в flex-контейнерах
  • Box Model: padding в em масштабируется вместе с шрифтом
  • Абсолютные единицы

    // px — пиксели (основная единица)
    // На Retina: 1 CSS px = 2 физических пикселя (devicePixelRatio = 2)
    // Используй для: border, box-shadow, border-radius, медиа-запросы
    
    // pt — типографская единица. 1pt = 1/72 дюйма ≈ 1.33px
    // Только для печати (print stylesheet)

    Относительные единицы

    // rem — от root font-size (html элемент, обычно 16px)
    // Предсказуем, не накапливается при вложении
    // Используй для: font-size, padding, margin, max-width
    
    // em — от ТЕКУЩЕГО font-size элемента
    // Накапливается при вложении — опасность!
    // Используй для: padding внутри кнопок (масштабируется со шрифтом кнопки)
    
    // % — от родительского элемента
    // width: 50% = половина родителя
    // padding-top: 50% = 50% от ШИРИНЫ (!) родителя — ловушка!
    
    // vh — 1% высоты viewport
    // vw — 1% ширины viewport
    // dvh — dynamic viewport height (учитывает мобильный браузер)
    // vmin — min(vw, vh), vmax — max(vw, vh)

    Когда что использовать

    | Единица | Лучше для |

    |---------|-----------|

    | rem | font-size, spacing, max-width компонентов |

    | px | border, shadow, border-radius, breakpoints |

    | % | width в flex/grid, fluid layout |

    | em | padding кнопок (относительно их шрифта) |

    | vh/vw | hero-секции, полноэкранные модалы |

    Ловушка: em-нестование

    // html: 16px (корень)
    // div { font-size: 1.5em }  → 24px
    // span { font-size: 1.5em } → 36px (1.5 × 24!)
    // p    { font-size: 1.5em } → 54px (1.5 × 36!)
    
    // rem не накапливается:
    // div { font-size: 1.5rem } → 24px (всегда от root)
    // span { font-size: 1.5rem } → 24px (независимо от родителя!)

    CSS calc() — комбинирование единиц

    // Комбинирование разных единиц — только так возможно
    // calc(100% - 40px)    — ширина минус отступы
    // calc(100vh - 64px)   — высота viewport минус шапка
    // calc(1rem + 2vw)     — адаптивный шрифт (Fluid Typography)
    // calc(50% - 160px)    — позиционирование по центру
    
    // Из JavaScript:
    // element.style.width = `calc(100% - ${sidebarWidth}px)`

    getComputedStyle в JavaScript

    // getComputedStyle ВСЕГДА возвращает вычисленные px-значения!
    const style = getComputedStyle(element)
    style.fontSize  // '16px'  — даже если задан 1rem
    style.width     // '320px' — даже если задан 20%
    
    // Конвертация px → rem:
    const pxSize  = parseFloat(style.fontSize)  // 16
    const rootPx  = parseFloat(getComputedStyle(document.documentElement).fontSize)
    const remSize = pxSize / rootPx  // 1

    Типичные ошибки

    Ошибка 1: px для font-size

    /* ПЛОХО — игнорирует настройки браузера пользователя */
    body { font-size: 16px; }
    
    /* ХОРОШО — масштабируется с браузером */
    body { font-size: 1rem; }

    Ошибка 2: em для font-size в компонентах

    /* ПЛОХО — накапливается при вложении */
    .card { font-size: 0.875em; }
    .card .inner { font-size: 0.875em; } /* → 0.875 × 0.875 = 0.766rem! */
    
    /* ХОРОШО — всегда предсказуемо */
    .card { font-size: 0.875rem; }
    .card .inner { font-size: 0.875rem; }

    Ошибка 3: vh без учёта мобильных браузеров

    /* ПЛОХО — на iOS 100vh включает адресную строку → обрезается */
    .hero { height: 100vh; }
    
    /* ХОРОШО — dvh учитывает динамический viewport */
    .hero { height: 100dvh; }
    /* или fallback: */
    .hero { height: 100vh; height: 100dvh; }

    В реальных проектах

  • Дизайн-системы: spacing scale в rem (4px = 0.25rem, 8px = 0.5rem, 16px = 1rem)
  • Fluid Typography: font-size: clamp(1rem, 1rem + 2vw, 1.5rem) — автоматически адаптируется
  • Полноэкранные секции: height: 100dvh для hero и модальных окон
  • JS-анимации: конвертация единиц для точных расчётов позиций
  • Примеры

    Конвертер CSS единиц: rem, em, px, vh, vw — вычисления и ловушки em-нестования

    // Конвертер CSS единиц — чистые вычисления без DOM
    
    // Конфигурация среды
    const env = {
      rootFontSize:   16,    // px (html font-size)
      viewportWidth:  1440,  // px
      viewportHeight: 900,   // px
      devicePixelRatio: 2,   // Retina
    }
    
    // ===== Конвертеры =====
    
    const convert = {
      remToPx: (rem, root = env.rootFontSize)  => rem * root,
      pxToRem: (px, root = env.rootFontSize)   => px / root,
      emToPx:  (em, parentSize)               => em * parentSize,
      vhToPx:  (vh, vh100 = env.viewportHeight) => (vh / 100) * vh100,
      vwToPx:  (vw, vw100 = env.viewportWidth)  => (vw / 100) * vw100,
      pctToPx: (pct, parentPx)               => (pct / 100) * parentPx,
    }
    
    // ===== Демонстрация =====
    
    console.log('=== rem ↔ px (root = 16px) ===')
    const remValues = [0.5, 0.75, 0.875, 1, 1.125, 1.25, 1.5, 2, 3]
    for (const rem of remValues) {
      console.log(`  ${String(rem).padEnd(6)}rem = ${convert.remToPx(rem)}px`)
    }
    
    console.log('\n=== px → rem ===')
    const pxValues = [8, 12, 14, 16, 18, 20, 24, 32, 48, 64]
    for (const px of pxValues) {
      const rem = convert.pxToRem(px)
      console.log(`  ${String(px).padStart(3)}px = ${rem.toFixed(4)}rem`)
    }
    
    // ===== EM: ловушка нестования =====
    console.log('\n=== em — ловушка нестования ===')
    const rootFont = 16
    
    let parentFont  = convert.emToPx(1.5, rootFont)    // html → div
    let childFont   = convert.emToPx(1.5, parentFont)  // div → span
    let grandChild  = convert.emToPx(1.5, childFont)   // span → p
    
    console.log(`  html  (root):     ${rootFont}px`)
    console.log(`  div   (1.5em):    ${parentFont}px   (×1.5 от root)`)
    console.log(`  span  (1.5em):    ${childFont}px   (×2.25 от root!)`)
    console.log(`  p     (1.5em):    ${grandChild}px  (×3.375 от root!!)`)
    console.log()
    console.log('  rem НЕ накапливается:')
    console.log(`  div   (1.5rem):   ${convert.remToPx(1.5)}px  (всегда от root)`)
    console.log(`  span  (1.5rem):   ${convert.remToPx(1.5)}px  (не зависит от родителя)`)
    
    // ===== vh/vw =====
    console.log(`\n=== vh/vw (viewport ${env.viewportWidth}×${env.viewportHeight}) ===`)
    const vhValues = [10, 25, 50, 100]
    const vwValues = [10, 25, 33, 50, 100]
    
    console.log('vh:')
    for (const vh of vhValues) {
      console.log(`  ${String(vh).padStart(4)}vh = ${convert.vhToPx(vh)}px`)
    }
    
    console.log('vw:')
    for (const vw of vwValues) {
      console.log(`  ${String(vw).padStart(4)}vw = ${convert.vwToPx(vw)}px`)
    }
    
    // ===== Типовая spacing scale (Tailwind/дизайн-системы) =====
    console.log('\n=== Spacing scale (base = 4px = 0.25rem) ===')
    const spacingScale = [
      [0, 0],    [1, 4],   [2, 8],   [3, 12],  [4, 16],
      [5, 20],   [6, 24],  [8, 32],  [10, 40], [12, 48],
      [16, 64],  [20, 80], [24, 96],
    ]
    for (const [name, px] of spacingScale) {
      const rem = convert.pxToRem(px)
      console.log(`  spacing-${String(name).padEnd(3)}: ${String(px).padStart(3)}px = ${rem.toFixed(4)}rem`)
    }
    
    // ===== calc() симуляция =====
    console.log('\n=== calc() применение ===')
    const examples = [
      { expr: '100vw - 2*24px',         result: convert.vwToPx(100) - 2 * 24 },
      { expr: '100vh - 64px',           result: convert.vhToPx(100) - 64 },
      { expr: '50% - 1rem (parent 1200px)', result: convert.pctToPx(50, 1200) - convert.remToPx(1) },
      { expr: '1rem + 2vw',             result: convert.remToPx(1) + convert.vwToPx(2) },
    ]
    for (const { expr, result } of examples) {
      console.log(`  calc(${expr}) = ${Math.round(result)}px`)
    }

    Задание

    Реализуй универсальный конвертер CSS-единиц. Реализуй: - `toPx(value, unit, context)` — конвертирует значение в пиксели. Поддерживает: `'px'`, `'rem'`, `'em'`, `'vh'`, `'vw'` - `fromPx(px, unit, context)` — конвертирует пиксели в нужную единицу - `convertUnit(value, fromUnit, toUnit, context)` — конвертирует между любыми единицами `context`: `{ rootFontSize, parentFontSize, viewportWidth, viewportHeight }`

    Подсказка

    toPx для rem: value * rootFontSize; для vh: (value/100)*viewportHeight. fromPx для rem: px/rootFontSize; для vh: (px/viewportHeight)*100. convertUnit: toPx(value, fromUnit), потом fromPx(px, toUnit)

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