Ты получаешь из дизайна размеры в px, а пользователь увеличил шрифт в браузере до 20px — весь интерфейс сломался. Или добавляешь em для отступов внутри компонента, вкладываешь его в другой компонент — и шрифт становится 48px вместо 12px. Понимание единиц CSS спасает от этих ловушек.
Разные единицы предназначены для разных задач. Неправильный выбор ведёт к неадаптивному интерфейсу, проблемам с доступностью (пользователи с большим шрифтом), и непредсказуемому поведению в разных контекстах.
element.style.width — как устанавливать и читать% для fluid layout в flex-контейнерах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-секции, полноэкранные модалы |
// 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 (независимо от родителя!)// Комбинирование разных единиц — только так возможно
// calc(100% - 40px) — ширина минус отступы
// calc(100vh - 64px) — высота viewport минус шапка
// calc(1rem + 2vw) — адаптивный шрифт (Fluid Typography)
// calc(50% - 160px) — позиционирование по центру
// Из JavaScript:
// element.style.width = `calc(100% - ${sidebarWidth}px)`// 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; }font-size: clamp(1rem, 1rem + 2vw, 1.5rem) — автоматически адаптируетсяheight: 100dvh для hero и модальных оконКонвертер 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`)
}Ты получаешь из дизайна размеры в px, а пользователь увеличил шрифт в браузере до 20px — весь интерфейс сломался. Или добавляешь em для отступов внутри компонента, вкладываешь его в другой компонент — и шрифт становится 48px вместо 12px. Понимание единиц CSS спасает от этих ловушек.
Разные единицы предназначены для разных задач. Неправильный выбор ведёт к неадаптивному интерфейсу, проблемам с доступностью (пользователи с большим шрифтом), и непредсказуемому поведению в разных контекстах.
element.style.width — как устанавливать и читать% для fluid layout в flex-контейнерах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-секции, полноэкранные модалы |
// 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 (независимо от родителя!)// Комбинирование разных единиц — только так возможно
// calc(100% - 40px) — ширина минус отступы
// calc(100vh - 64px) — высота viewport минус шапка
// calc(1rem + 2vw) — адаптивный шрифт (Fluid Typography)
// calc(50% - 160px) — позиционирование по центру
// Из JavaScript:
// element.style.width = `calc(100% - ${sidebarWidth}px)`// 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; }font-size: clamp(1rem, 1rem + 2vw, 1.5rem) — автоматически адаптируетсяheight: 100dvh для hero и модальных оконКонвертер 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)