HTML & CSS

CSS Best Practices: типові ситуації та правильні рішення

Збірка найпоширеніших CSS-помилок і правильних підходів до їх вирішення. Кожна ситуація показана з прикладом «як не треба» і «як правильно» — з детальним поясненням причини та живою демонстрацією.

CSS Best Practices: типові ситуації та правильні рішення

Звідки беруться ці проблеми

Більшість CSS-помилок — не через незнання властивостей. Розробник знає що таке margin, flex, position. Проблема у тому, що CSS має неочевидну поведінку: засновану на алгоритмах браузера, які ніхто не пояснував.

Ця стаття — практичний довідник. Кожен розділ: реальна ситуаціяпричинарішенняdemо. Формат навмисно прямий — без зайвої теорії там, де достатньо побачити результат.


Блок 1: Відступи та spacing

1.1 Висячий margin у останнього елемента

Ситуація: Список карток або пунктів — у кожного margin-bottom: 1rem. Останній елемент має зайвий відступ знизу, який «з'їдає» простір батьківського контейнера або залишає порожнечу.

/* ❌ Проблема */
.card { margin-bottom: 1.5rem; }
/* Останній .card теж має margin-bottom — зайвий відступ */

Рішення 1 — Очевидне (але найгірше):

/* ❌ Поширено, але хибно — дублює логіку */
.card { margin-bottom: 1.5rem; }
.card:last-child { margin-bottom: 0; }

Рішення 2 — Lobotomized Owl (Stack pattern):

/* ✅ Відступ лише МІЖ елементами — не перед першим і не після останнього */
.card-list > * + * { margin-top: 1.5rem; }

Рішення 3 — Найкраще: gap у flex/grid:

/* ✅ gap існує тільки між елементами, ніколи не зовні */
.card-list {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}
🔒localhost:3000

1.2 margin-top і margin-bottom одночасно — margin collapsing пастка

Ситуація: Елементи мають і margin-top, і margin-bottom. При сусідстві відступи «зливаються» (collapse) — замість 32px виходить 16px. Або навпаки — відступи подвоюються там, де їх не очікували.

/* ❌ Непослідовно — важко передбачити результат */
h2 { margin-top: 2rem; margin-bottom: 1rem; }
p  { margin-top: 1rem; margin-bottom: 1rem; }
/* Між h2 та p: max(1rem, 1rem) = 1rem — не 2rem як очікували! */

Правило: в рамках одного компонента або потоку — завжди лише один напрямок. Спільноприйнятий стандарт — margin-bottom (або логічний margin-block-end).

/* ✅ Single-direction: тільки margin-bottom */
h1 { margin-bottom: 1.5rem; }
h2 { margin-bottom: 1rem; }
p  { margin-bottom: 1rem; }
ul { margin-bottom: 1rem; }

/* Або ще краще — через Owl на контейнері */
.prose > * + * { margin-top: 1em; }
🔒localhost:3000

1.3 margin: 0 auto не центрує елемент

Ситуація: Задано margin: 0 auto і нічого не відбувається. Блок не центрується.

Причина: auto ділить вільний простір. Якщо елемент займає width: 100% (за замовчуванням для блокових елементів) — вільного простору немає. Центрування не відбудеться.

/* ❌ div займає 100% ширини — auto = 0 */
div { margin: 0 auto; } /* не працює! */

/* ✅ Потрібна явна ширина */
div { margin: 0 auto; max-width: 800px; }

/* ✅ Або inline-block */
span { display: inline-block; margin: 0 auto; } /* теж не спрацює без ширини батька */

/* ✅ Сучасний варіант */
div { margin-inline: auto; max-width: min(800px, 100%); }

1.4 Горизонтальні відступи між flex-елементами — зайві краї

Ситуація: Сітка карток з margin-right на кожній — у першій та останній у рядку зайві відступи залишаються або відрізаються.

/* ❌ Класична проблема з margin-right/left у рядах */
.card { margin-right: 1rem; }
.card:last-child { margin-right: 0; }
/* А якщо рядки переносяться? :last-child не рятує */
/* ✅ gap вирішує все */
.cards { display: flex; flex-wrap: wrap; gap: 1rem; }
.card  { flex: 1 1 200px; }

Блок 2: Flexbox пастки

2.1 Flex-елемент несподівано розтягується по висоті

Ситуація: Картки складені у flex-рядок. Маленька картка розтягнулась на висоту найбільшої — хоча контент не потребує такої висоти.

Причина: align-items за замовчуванням — stretch. Усі flex-items розтягуються до висоти найвищого.

🔒localhost:3000
/* ✅ Рішення */
.flex-row { display: flex; gap: 1rem; align-items: flex-start; }
/* або для конкретного елемента */
.small-card { align-self: flex-start; }

2.2 min-width: 0 — текст не переноситься у flex-item

Ситуація: Текст у flex-елементі виходить за межі контейнера і ламає layout.

Причина: За специфікацією flex-items мають min-width: auto — вони не можуть бути меншими за мінімальний розмір свого вмісту. Для тексту це ширина найдовшого слова.

🔒localhost:3000
/* ✅ Рішення: min-width: 0 на flex-item */
.flex-item {
  flex: 1;
  min-width: 0; /* дозволяє стискатись нижче auto */
  overflow: hidden;
  text-overflow: ellipsis;
}
/* або для перенесення тексту */
.flex-item {
  min-width: 0;
  overflow-wrap: break-word;
  word-break: break-word;
}

2.3 justify-content: space-between — останній рядок не вирівнюється

Ситуація: Сітка карток з flex-wrap: wrap та justify-content: space-between. Якщо останній рядок неповний — картки «розтягуються» по крайніх позиціях, залишаючи дірки.

🔒localhost:3000
/* ✅ Рішення 1: Grid замість flex для сіток */
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; }

/* ✅ Рішення 2: Невидимий spacer (хак, але іноді єдиний варіант) */
.flex-grid::after { content: ''; flex: 0 0 200px; }

/* ✅ Рішення 3: justify-content: flex-start + gap (якщо не потрібен space-between) */
.flex-grid { display: flex; flex-wrap: wrap; gap: 1rem; justify-content: flex-start; }
.flex-grid > * { flex: 0 0 calc(33.33% - 0.67rem); }

Блок 3: Позиціонування

3.1 position: absolute не прив'язується до батька

Ситуація: Елемент з position: absolute позиціонується не відносно батька, а відносно viewport або якогось далекого предка.

Причина: absolute позиціонується відносно найближчого позиціонованого предкаposition відмінним від static). Якщо такого немає — відносно Initial Containing Block (зазвичай <html>).

🔒localhost:3000
/* ✅ Завжди: якщо хочете absolute відносно батька — задайте position: relative батькові */
.card {
  position: relative; /* контекст для абсолютно позиціонованих дочірніх */
}
.card__badge {
  position: absolute;
  top: -8px;
  right: -8px;
}

3.2 position: sticky не «прилипає»

Ситуація: Задано position: sticky; top: 0 — але елемент не прилипає при скролі.

Причини (найпоширеніші):

  1. Батько має overflow: hidden, auto або scroll — sticky прив'язується до скрол-контейнера, а якщо батько ним є і не скролиться сам — ефекту немає
  2. Не задано жодне з: top, bottom, left, right — обов'язково потрібно хоча б одне
  3. Батько занадто малий — sticky «прилипає» до батька, якщо батько вже завершився — елемент іде разом
/* ❌ Поширена пастка */
.wrapper {
  overflow: hidden; /* вбиває sticky всередині! */
}
.sticky-nav {
  position: sticky;
  top: 0; /* є, але не працює через батька */
}

/* ✅ Рішення: прибрати overflow: hidden з батька */
.wrapper {
  /* overflow: hidden — видалити або замінити на clip якщо потрібно */
  overflow: clip; /* clip не впливає на sticky! */
}

3.3 z-index не працює

Ситуація: Заданий z-index: 9999, але елемент все одно під іншим.

Причина: z-index працює лише між елементами одного stacking context. Кожен stacking context — це «окрема всесвіт». Якщо батько елемента має нижчий z-index у своєму контексті — дочірній не може «вирватись» вище.

Що створює новий stacking context:

  • position + z-index (не auto)
  • opacity < 1
  • transform, filter, clip-path, mask
  • isolation: isolate
  • will-change з певними значеннями
/* ✅ Рішення: isolation: isolate для явного контролю */
.modal-backdrop {
  isolation: isolate; /* окремий stacking context */
  z-index: 100;
}

/* ✅ Якщо потрібно повністю виключити вплив transform на z-index */
.card {
  transform: translateY(0); /* створює stacking context! */
  isolation: isolate;
}

Блок 4: Розміри та overflow

4.1 width: 100% + padding = елемент ширший за контейнер

Ситуація: Елемент з width: 100% і padding: 1rem — виходить за межі батька.

Причина: За замовчуванням box-sizing: content-boxwidth: 100% = ширина контенту, потім додається padding.

🔒localhost:3000
/* ✅ Глобальний reset — найважливіший рядок у вашому CSS */
*, *::before, *::after {
  box-sizing: border-box;
}

4.2 Зображення «розтягується» у flex-контейнері

Ситуація: <img> всередині flex-контейнера розтягується по висоті — пропорції порушуються.

Причина: Flex align-items: stretch + браузер трактує <img> як inline-елемент у певному контексті.

/* ✅ Варіант 1: align-self на зображенні */
img { align-self: flex-start; }

/* ✅ Варіант 2: object-fit для заповнення без деформації */
.card__image {
  width: 100%;
  height: 200px;
  object-fit: cover;  /* вирізає і масштабує без деформації */
  object-position: center; /* фокус кадру */
  display: block; /* прибирає inline gap знизу */
}

/* ✅ Варіант 3: aspect-ratio */
.card__image {
  width: 100%;
  aspect-ratio: 16/9;
  object-fit: cover;
}

Блок 5: Текст та типографія

5.1 Рядки занадто довгі — max-width: 65ch

Ситуація: На широких екранах рядки тексту займають всю ширину сторінки — 1200px+. Текст стає нечитабельним.

Дослідження: Оптимальна довжина рядка для читання — 45–75 символів. ch — ширина символу 0 у поточному шрифті, тому max-width: 65ch точно відповідає типографічному стандарту.

/* ✅ Для текстового контенту */
.article__body {
  max-width: 65ch;
  margin-inline: auto; /* центрування */
}

/* ✅ Fluid варіант: min(65ch, 100%) — ніколи не ширший за вміщення */
.text-content {
  width: min(65ch, 100%);
  margin-inline: auto;
}
🔒localhost:3000

5.2 font-size: 62.5% на :root — шкідливий трюк

Ситуація: Поширений «трюк» — задати html { font-size: 62.5% }, щоб 1rem = 10px і легше робити математику. 1.6rem = 16px, 2.4rem = 24px.

Проблема: Якщо користувач збільшив базовий шрифт браузера (наприклад, до 20px для кращої читабельності), 62.5% перемножується на 20px = 12.5px замість очікуваних 10px. Вся математика ламається. Плюс — порушується доступність.

/* ❌ Поганий трюк */
html { font-size: 62.5%; } /* 10px — але залежить від налаштувань браузера! */
body { font-size: 1.6rem; } /* 16px — але лише якщо браузер = 16px */

/* ✅ Просто використовуйте rem без трюків */
html { /* нічого — дефолт браузера */ }
body { font-size: 1rem; }   /* 16px (або скільки задав користувач) */
h1   { font-size: 2rem; }   /* 32px (або пропорційно) */

5.3 Текст виходить за межі картки

Ситуація: Довге слово (URL, email, технічний термін) «зламує» картку — текст виходить за неї.

/* ✅ Комбінація для коректного переносу тексту */
.card {
  overflow-wrap: break-word; /* переносить якщо слово занадто довге */
  word-break: break-word;    /* старий fallback для Safari */
  hyphens: auto;             /* додає дефіси при переносі (де підтримується) */
}

/* ✅ Для однорядкового truncate */
.card__title {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* або сучасний multi-line clamp: */
}

/* ✅ Multi-line truncate */
.card__desc {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3; /* максимум 3 рядки */
  overflow: hidden;
}

Блок 6: Селектори та специфічність

6.1 !important скрізь — архітектурна помилка

Ситуація: Стилі «не перевизначаються», розробник починає додавати !important. Згодом !important стає нормою, а перевизначити його стає ще складніше.

Причина: Надто висока специфічність у базових стилях.

/* ❌ Початок кінця */
.button          { color: white !important; }
.button--danger  { color: white !important; background: red !important; }

/* ✅ Правильна архітектура: низька специфічність → легко перевизначати */
/* Використовуйте @layer */
@layer base, components, utilities;

@layer components {
  .button { color: white; background: #6366f1; }
}

/* Утиліти мають вищий пріоритет за компоненти — без !important */
@layer utilities {
  .bg-red { background: red; }
}

6.2 Стилі для кнопки відрізняються між браузерами

Ситуація: <button> виглядає по-різному в Chrome, Firefox, Safari — різний border, padding, font.

/* ✅ Мінімальний reset для кнопок */
button {
  appearance: none;      /* скидає нативний стиль */
  border: none;
  background: none;
  padding: 0;
  cursor: pointer;
  font: inherit;         /* успадковує font від батька */
  color: inherit;
  line-height: 1;
  -webkit-appearance: none;
}

/* ✅ Або глобальний Modern CSS Reset (Andy Bell) */
button, input, select, textarea {
  font: inherit;
}

6.3 Кнопка «невидимого» синього кольору — не скинуто колір посилання

Ситуація: <a> без класу завжди синій та підкреслений. Навіть у навігації — якщо не перевизначити.

/* ✅ Скидайте стилі посилань там, де потрібно */
.nav__link {
  color: inherit;       /* колір від батька */
  text-decoration: none;
}

.nav__link:hover {
  text-decoration: underline; /* повертаємо де треба */
}

/* ❌ Не робіть global скид — це ламає доступність */
/* a { color: inherit; text-decoration: none; } — занадто агресивно */

Блок 7: Адаптивність

7.1 Картинки виходять за межі контейнера на мобільних

Ситуація: Зображення зберігають свою природну ширину — на мобільних виходять за межі.

/* ✅ Базовий reset для медіа — додавайте завжди */
img, video, svg, canvas {
  max-width: 100%;      /* ніколи не ширше за контейнер */
  height: auto;         /* зберігає пропорції */
  display: block;       /* прибирає inline gap знизу */
}

7.2 Картки не мають рівної висоти в рядку

Ситуація: Картки з різним обсягом тексту — кнопка «Детальніше» у різних місцях.

🔒localhost:3000
/* ✅ Рішення: flex column у картці + flex: 1 на тексті */
.card {
  display: flex;
  flex-direction: column;
}
.card__body {
  flex: 1; /* займає весь вільний простір, тягне кнопку вниз */
}
.card__footer {
  margin-top: auto; /* альтернативний варіант */
}

7.3 Адаптивна сітка без media queries

/* ✅ The RAM — Repeat Auto Minmax */
.grid {
  display: grid;
  /* auto-fill: стільки колонок скільки вміщується */
  /* min(250px, 100%): на маленьких екранах — одна колонка */
  grid-template-columns: repeat(auto-fill, minmax(min(250px, 100%), 1fr));
  gap: 1rem;
}

Блок 8: Загальні практики

8.1 Modern CSS Reset

Не * { margin: 0; padding: 0 } — це надто агресивно і ламає форми. Сучасний reset Енді Белла:

/* Andy Bell's Modern CSS Reset */
*, *::before, *::after {
  box-sizing: border-box;
}

* {
  margin: 0;
}

body {
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

img, picture, video, canvas, svg {
  display: block;
  max-width: 100%;
}

input, button, textarea, select {
  font: inherit;
}

p, h1, h2, h3, h4, h5, h6 {
  overflow-wrap: break-word;
}

#root, #__next {
  isolation: isolate;
}

8.2 Magic numbers → Custom Properties

Ситуація: CSS повний «магічних чисел» — margin-top: 37px, padding: 13px 22px. При зміні дизайну потрібно шукати і замінювати по всьому файлу.

/* ❌ Magic numbers */
.hero { padding-top: 96px; }
.section { padding-top: 64px; }
.card { padding: 24px; }

/* ✅ CSS Custom Properties + spacing scale */
:root {
  --space-1: 0.25rem;  /* 4px */
  --space-2: 0.5rem;   /* 8px */
  --space-3: 0.75rem;  /* 12px */
  --space-4: 1rem;     /* 16px */
  --space-6: 1.5rem;   /* 24px */
  --space-8: 2rem;     /* 32px */
  --space-12: 3rem;    /* 48px */
  --space-16: 4rem;    /* 64px */
  --space-24: 6rem;    /* 96px */
}

.hero    { padding-top: var(--space-24); }
.section { padding-top: var(--space-16); }
.card    { padding: var(--space-6); }

8.3 currentColor — успадкування кольору

/* ❌ Дублювання кольору */
.icon-btn { color: #6366f1; }
.icon-btn svg { fill: #6366f1; stroke: #6366f1; } /* дублюємо */

/* ✅ currentColor успадковує колір батька */
.icon-btn { color: #6366f1; }
.icon-btn svg { fill: currentColor; } /* автоматично #6366f1 */

/* ✅ При hover — оновиться автоматично */
.icon-btn:hover { color: #4f46e5; }
/* svg теж стане #4f46e5 без додаткових правил */

8.4 CSS-пастка: display: inline блокує width/height

Ситуація: Задано width: 200px для <span> — не працює.

/* ❌ inline не підтримує width/height */
span { width: 200px; height: 50px; } /* ігнорується */

/* ✅ Змінити display */
span { display: inline-block; width: 200px; height: 50px; }
/* або */
span { display: block; }
/* або для flex-layouts */
span { display: flex; }

Чеклист перед здачею верстки

📐 Відступи

  • gap замість margin-right/bottom у flex/grid
  • Single-direction margins (або Owl selector)
  • box-sizing: border-box глобально
  • Останній елемент без «висячого» margin

💪 Flexbox

  • min-width: 0 на flex-items з текстом
  • align-items: flex-start якщо не потрібен stretch
  • gap замість margin між flex-елементами
  • flex-wrap: wrap де потрібно

📌 Позиціонування

  • position: relative на батьку для absolute дитини
  • overflow: clip замість hidden якщо є sticky
  • isolation: isolate для контролю stacking context
  • z-index — не більше 3–4 рівнів у проєкті

🖼️ Зображення

  • max-width: 100%; height: auto; display: block
  • object-fit: cover + aspect-ratio для рівної висоти
  • loading="lazy" для off-screen зображень
  • width і height атрибути у HTML для CLS

🔤 Текст

  • max-width: 65ch для читабельних рядків
  • overflow-wrap: break-word для довгих слів
  • -webkit-line-clamp для multi-line truncate
  • Без 62.5% трюку на :root

🏗️ Архітектура

  • Modern CSS Reset
  • CSS Custom Properties для кольорів і відступів
  • @layer для керування пріоритетом
  • Без !important у компонентах