Більшість CSS-помилок — не через незнання властивостей. Розробник знає що таке margin, flex, position. Проблема у тому, що CSS має неочевидну поведінку: засновану на алгоритмах браузера, які ніхто не пояснував.
Ця стаття — практичний довідник. Кожен розділ: реальна ситуація → причина → рішення → demо. Формат навмисно прямий — без зайвої теорії там, де достатньо побачити результат.
Ситуація: Список карток або пунктів — у кожного 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;
}
<div class="hanging-margin-demo">
<div class="hm-case">
<p class="case-label">❌ margin-bottom на кожному</p>
<div class="hm-container hm-bad">
<div class="hm-card">Картка 1</div>
<div class="hm-card">Картка 2</div>
<div class="hm-card">Картка 3 (з висячим margin!)</div>
</div>
<p class="hm-note">Червона зона — зайвий відступ після останнього</p>
</div>
<div class="hm-case">
<p class="case-label">✅ gap — відступ тільки між</p>
<div class="hm-container hm-good">
<div class="hm-card hm-card--green">Картка 1</div>
<div class="hm-card hm-card--green">Картка 2</div>
<div class="hm-card hm-card--green">Картка 3 (без зайвого!)</div>
</div>
</div>
</div>
.hanging-margin-demo {
display: flex;
gap: 1rem;
flex-wrap: wrap;
padding: 1rem;
background: #f8fafc;
font-family: system-ui, sans-serif;
font-size: 0.85rem;
}
.hm-case { flex: 1; min-width: 200px; }
.case-label { margin: 0 0 0.5rem; font-weight: 700; font-size: 0.8rem; }
.hm-container {
border-radius: 8px;
border: 2px solid;
overflow: visible;
}
.hm-bad { border-color: #fca5a5; background: #fff5f5; padding: 0.75rem 0.75rem 0; }
.hm-good { border-color: #86efac; background: #f0fdf4; padding: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem; }
.hm-card {
background: white;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 0.5rem 0.75rem;
font-size: 0.8rem;
}
.hm-bad .hm-card { margin-bottom: 0.5rem; }
.hm-bad .hm-card:last-child {
background: #fef2f2;
border-color: #fca5a5;
position: relative;
}
.hm-bad .hm-card:last-child::after {
content: '← зайвий margin-bottom';
display: block;
height: 0;
line-height: 0;
font-size: 0.65rem;
color: #ef4444;
margin-top: 0.5rem;
margin-bottom: -0.3rem;
}
.hm-bad { padding-bottom: 1.25rem; }
.hm-card--green { background: #fff; border-color: #86efac; }
.hm-note { margin: 0.3rem 0 0; font-size: 0.72rem; color: #ef4444; font-style: italic; }
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; }
<div class="collapse-demo">
<div class="cd-case">
<p class="case-label">❌ margin-top + margin-bottom (collapse)</p>
<div class="cd-box cd-bad">
<h3 style="margin:1rem 0">Заголовок (margin: 1rem 0)</h3>
<p style="margin:1rem 0">Параграф (margin: 1rem 0). Відступ між ними = 1rem (collapse), не 2rem як ви, можливо, думали.</p>
<p style="margin:1rem 0">Другий параграф. Відступ між ними теж 1rem.</p>
</div>
</div>
<div class="cd-case">
<p class="case-label">✅ Тільки margin-bottom (передбачувано)</p>
<div class="cd-box cd-good">
<h3 style="margin:0 0 0.75rem">Заголовок (margin-bottom: 0.75rem)</h3>
<p style="margin:0 0 0.75rem">Параграф (margin-bottom: 0.75rem). Відступ рівно 0.75rem — завжди.</p>
<p style="margin:0">Останній параграф — margin: 0.</p>
</div>
</div>
</div>
.collapse-demo {
display: flex; gap: 0.75rem; flex-wrap: wrap;
padding: 1rem; background: #f8fafc;
font-family: system-ui, sans-serif; font-size: 0.85rem;
}
.cd-case { flex: 1; min-width: 200px; }
.case-label { margin: 0 0 0.4rem; font-weight: 700; font-size: 0.78rem; }
.cd-box { border-radius: 8px; border: 2px solid; padding: 0.75rem; background: white; }
.cd-bad { border-color: #fca5a5; }
.cd-good { border-color: #86efac; }
.cd-box h3 { color: #6366f1; font-size: 0.95rem; }
.cd-box p { font-size: 0.8rem; color: #374151; line-height: 1.4; }
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%); }
Ситуація: Сітка карток з 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; }
Ситуація: Картки складені у flex-рядок. Маленька картка розтягнулась на висоту найбільшої — хоча контент не потребує такої висоти.
Причина: align-items за замовчуванням — stretch. Усі flex-items розтягуються до висоти найвищого.
<div class="stretch-demo">
<div class="sd-case">
<p class="case-label">❌ align-items: stretch (дефолт)</p>
<div class="sd-flex sd-bad">
<div class="sd-card">Маленька картка</div>
<div class="sd-card sd-tall">Висока картка<br>з більшим<br>вмістом</div>
<div class="sd-card">Ще одна маленька — розтягнута!</div>
</div>
</div>
<div class="sd-case">
<p class="case-label">✅ align-items: flex-start</p>
<div class="sd-flex sd-good">
<div class="sd-card sd-green">Маленька картка</div>
<div class="sd-card sd-green sd-tall">Висока картка<br>з більшим<br>вмістом</div>
<div class="sd-card sd-green">Природна висота ✅</div>
</div>
</div>
</div>
.stretch-demo {
display: flex; flex-direction: column; gap: 0.75rem;
padding: 1rem; background: #f8fafc;
font-family: system-ui, sans-serif; font-size: 0.85rem;
}
.case-label { margin: 0 0 0.4rem; font-weight: 700; font-size: 0.78rem; }
.sd-flex { display: flex; gap: 0.5rem; }
.sd-bad { align-items: stretch; /* дефолт */ }
.sd-good { align-items: flex-start; }
.sd-card {
background: white; border: 1.5px solid #fca5a5;
border-radius: 6px; padding: 0.6rem 0.75rem;
font-size: 0.78rem; flex: 1; color: #7f1d1d;
}
.sd-green { border-color: #86efac; color: #14532d; }
.sd-tall { /* нічого — просто більше тексту */ }
/* ✅ Рішення */
.flex-row { display: flex; gap: 1rem; align-items: flex-start; }
/* або для конкретного елемента */
.small-card { align-self: flex-start; }
min-width: 0 — текст не переноситься у flex-itemСитуація: Текст у flex-елементі виходить за межі контейнера і ламає layout.
Причина: За специфікацією flex-items мають min-width: auto — вони не можуть бути меншими за мінімальний розмір свого вмісту. Для тексту це ширина найдовшого слова.
<div class="minw-demo">
<p class="case-label">❌ Без min-width: 0 — текст виходить за межі</p>
<div class="mw-flex mw-bad">
<div class="mw-sidebar">Sidebar</div>
<div class="mw-main mw-overflow">
ДужеДовгийТекстБезПробілівЩоВиходитьЗаМежіКонтейнера
</div>
</div>
<p class="case-label" style="margin-top: 0.75rem">✅ З min-width: 0 — обрізається коректно</p>
<div class="mw-flex mw-good">
<div class="mw-sidebar mw-sidebar--green">Sidebar</div>
<div class="mw-main mw-fix">
ДужеДовгийТекстБезПробілівЩоТепер КоректноОбрізається ✅
</div>
</div>
</div>
.minw-demo {
padding: 1rem; background: #f8fafc;
font-family: system-ui, sans-serif; font-size: 0.85rem; color: #1e293b;
}
.mw-flex { display: flex; gap: 0.5rem; overflow: hidden; }
.mw-sidebar {
width: 80px; flex-shrink: 0; background: #6366f1; color: white;
border-radius: 6px; padding: 0.5rem; text-align: center; font-size: 0.8rem;
}
.mw-sidebar--green { background: #10b981; }
.mw-main {
flex: 1; background: white; border: 1.5px solid;
border-radius: 6px; padding: 0.5rem; font-size: 0.78rem;
}
.mw-overflow { border-color: #fca5a5; overflow: visible; word-break: normal; overflow-wrap: normal; }
.mw-fix { border-color: #86efac; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ✅ Рішення: 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;
}
justify-content: space-between — останній рядок не вирівнюєтьсяСитуація: Сітка карток з flex-wrap: wrap та justify-content: space-between. Якщо останній рядок неповний — картки «розтягуються» по крайніх позиціях, залишаючи дірки.
<div class="sb-demo">
<p class="case-label">❌ justify-content: space-between з 5 картками</p>
<div class="sb-bad">
<div class="sb-card">1</div><div class="sb-card">2</div>
<div class="sb-card">3</div><div class="sb-card">4</div>
<div class="sb-card">5</div>
<!-- 5я картка — крайня зліва, пустота справа -->
</div>
<p class="case-label" style="margin-top: 0.75rem">✅ grid auto-fill — завжди вирівняно</p>
<div class="sb-good">
<div class="sb-card sb-card--green">1</div><div class="sb-card sb-card--green">2</div>
<div class="sb-card sb-card--green">3</div><div class="sb-card sb-card--green">4</div>
<div class="sb-card sb-card--green">5</div>
</div>
</div>
.sb-demo {
padding: 1rem; background: #f8fafc;
font-family: system-ui, sans-serif; font-size: 0.85rem;
}
.case-label { margin: 0 0 0.4rem; font-weight: 700; font-size: 0.78rem; }
.sb-bad {
display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.5rem;
}
.sb-good {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(90px, 100%), 1fr));
gap: 0.5rem;
}
.sb-card {
width: 90px; height: 50px;
background: white; border: 1.5px solid #fca5a5;
border-radius: 6px; display: flex; align-items: center; justify-content: center;
font-weight: 800; font-size: 1rem; color: #ef4444;
}
.sb-card--green { border-color: #86efac; color: #10b981; }
/* ✅ Рішення 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); }
position: absolute не прив'язується до батькаСитуація: Елемент з position: absolute позиціонується не відносно батька, а відносно viewport або якогось далекого предка.
Причина: absolute позиціонується відносно найближчого позиціонованого предка (з position відмінним від static). Якщо такого немає — відносно Initial Containing Block (зазвичай <html>).
<div class="abs-demo">
<div class="abs-case">
<p class="case-label">❌ Батько без position</p>
<div class="abs-parent abs-bad-parent">
Батько (без position)
<div class="abs-child abs-child-bad">absolute child</div>
</div>
</div>
<div class="abs-case">
<p class="case-label">✅ Батько з position: relative</p>
<div class="abs-parent abs-good-parent">
Батько (position: relative)
<div class="abs-child abs-child-good">absolute child ✅</div>
</div>
</div>
</div>
.abs-demo {
display: flex; gap: 1rem; flex-wrap: wrap;
padding: 1rem; background: #f8fafc;
font-family: system-ui, sans-serif; font-size: 0.82rem;
}
.abs-case { flex: 1; min-width: 180px; }
.case-label { margin: 0 0 0.4rem; font-weight: 700; font-size: 0.78rem; }
.abs-parent {
height: 80px; border-radius: 8px; padding: 0.5rem;
font-size: 0.75rem; font-weight: 700;
}
.abs-bad-parent { background: #fee2e2; border: 2px dashed #ef4444; color: #991b1b; }
.abs-good-parent { background: #dcfce7; border: 2px solid #10b981; color: #14532d; position: relative; }
.abs-child {
position: absolute;
bottom: 8px; right: 8px;
padding: 0.3rem 0.5rem;
border-radius: 4px;
font-size: 0.72rem;
font-weight: 700;
}
.abs-child-bad { background: #ef4444; color: white; }
.abs-child-good { background: #10b981; color: white; }
/* ✅ Завжди: якщо хочете absolute відносно батька — задайте position: relative батькові */
.card {
position: relative; /* контекст для абсолютно позиціонованих дочірніх */
}
.card__badge {
position: absolute;
top: -8px;
right: -8px;
}
position: sticky не «прилипає»Ситуація: Задано position: sticky; top: 0 — але елемент не прилипає при скролі.
Причини (найпоширеніші):
overflow: hidden, auto або scroll — sticky прив'язується до скрол-контейнера, а якщо батько ним є і не скролиться сам — ефекту немаєtop, bottom, left, right — обов'язково потрібно хоча б одне/* ❌ Поширена пастка */
.wrapper {
overflow: hidden; /* вбиває sticky всередині! */
}
.sticky-nav {
position: sticky;
top: 0; /* є, але не працює через батька */
}
/* ✅ Рішення: прибрати overflow: hidden з батька */
.wrapper {
/* overflow: hidden — видалити або замінити на clip якщо потрібно */
overflow: clip; /* clip не впливає на sticky! */
}
z-index не працюєСитуація: Заданий z-index: 9999, але елемент все одно під іншим.
Причина: z-index працює лише між елементами одного stacking context. Кожен stacking context — це «окрема всесвіт». Якщо батько елемента має нижчий z-index у своєму контексті — дочірній не може «вирватись» вище.
Що створює новий stacking context:
position + z-index (не auto)opacity < 1transform, filter, clip-path, maskisolation: isolatewill-change з певними значеннями/* ✅ Рішення: isolation: isolate для явного контролю */
.modal-backdrop {
isolation: isolate; /* окремий stacking context */
z-index: 100;
}
/* ✅ Якщо потрібно повністю виключити вплив transform на z-index */
.card {
transform: translateY(0); /* створює stacking context! */
isolation: isolate;
}
width: 100% + padding = елемент ширший за контейнерСитуація: Елемент з width: 100% і padding: 1rem — виходить за межі батька.
Причина: За замовчуванням box-sizing: content-box — width: 100% = ширина контенту, потім додається padding.
<div class="boxsizing-demo">
<p class="case-label">❌ box-sizing: content-box (дефолт)</p>
<div class="bsd-container">
<div class="bsd-bad">width: 100% + padding: 1rem = ширший за батька!</div>
</div>
<p class="case-label" style="margin-top: 0.75rem">✅ box-sizing: border-box</p>
<div class="bsd-container">
<div class="bsd-good">width: 100% + padding: 1rem = рівно 100% ✅</div>
</div>
</div>
.boxsizing-demo {
padding: 1rem; background: #f8fafc;
font-family: system-ui, sans-serif; font-size: 0.82rem;
}
.case-label { margin: 0 0 0.3rem; font-weight: 700; font-size: 0.78rem; }
.bsd-container {
border: 2px solid #64748b; border-radius: 6px;
overflow: hidden; margin-bottom: 0.5rem;
background: #e2e8f0; padding: 0;
}
.bsd-bad {
box-sizing: content-box;
width: 100%; padding: 0.6rem 1rem;
background: #fca5a5; border-radius: 4px;
font-size: 0.78rem; color: #7f1d1d;
}
.bsd-good {
box-sizing: border-box;
width: 100%; padding: 0.6rem 1rem;
background: #86efac; border-radius: 4px;
font-size: 0.78rem; color: #14532d;
}
/* ✅ Глобальний reset — найважливіший рядок у вашому CSS */
*, *::before, *::after {
box-sizing: border-box;
}
Ситуація: <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;
}
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;
}
<div class="linewidth-demo">
<div class="lw-bad">
<strong>❌ Без обмеження ширини</strong>
<p>Цей текст займає всю ширину контейнера. На широких екранах рядки стають настільки довгими, що очам доводиться долати великі відстані між рядками — читати важко і стомлює.</p>
</div>
<div class="lw-good">
<strong>✅ max-width: 55ch</strong>
<p>Цей текст обмежений по ширині. Рядки комфортні для читання — очі легко переходять від одного рядка до наступного.</p>
</div>
</div>
.linewidth-demo {
padding: 1rem; background: #f8fafc;
font-family: system-ui, sans-serif; font-size: 0.85rem;
display: flex; flex-direction: column; gap: 0.75rem;
}
.lw-bad, .lw-good {
background: white; border-radius: 8px; border: 1.5px solid;
padding: 0.75rem;
}
.lw-bad { border-color: #fca5a5; }
.lw-good { border-color: #86efac; }
.lw-bad strong { color: #991b1b; display: block; margin-bottom: 0.4rem; }
.lw-good strong { color: #14532d; display: block; margin-bottom: 0.4rem; }
.lw-bad p { margin: 0; line-height: 1.6; color: #374151; }
.lw-good p { margin: 0; line-height: 1.6; color: #374151; max-width: 55ch; }
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 (або пропорційно) */
Ситуація: Довге слово (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;
}
!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; }
}
Ситуація: <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;
}
Ситуація: <a> без класу завжди синій та підкреслений. Навіть у навігації — якщо не перевизначити.
/* ✅ Скидайте стилі посилань там, де потрібно */
.nav__link {
color: inherit; /* колір від батька */
text-decoration: none;
}
.nav__link:hover {
text-decoration: underline; /* повертаємо де треба */
}
/* ❌ Не робіть global скид — це ламає доступність */
/* a { color: inherit; text-decoration: none; } — занадто агресивно */
Ситуація: Зображення зберігають свою природну ширину — на мобільних виходять за межі.
/* ✅ Базовий reset для медіа — додавайте завжди */
img, video, svg, canvas {
max-width: 100%; /* ніколи не ширше за контейнер */
height: auto; /* зберігає пропорції */
display: block; /* прибирає inline gap знизу */
}
Ситуація: Картки з різним обсягом тексту — кнопка «Детальніше» у різних місцях.
<div class="equal-height-demo">
<p class="case-label">❌ Кнопки у різних місцях</p>
<div class="ehd-flex ehd-bad">
<div class="ehd-card">
<p class="ehd-text">Коротко</p>
<button class="ehd-btn">Детальніше</button>
</div>
<div class="ehd-card">
<p class="ehd-text">Значно довший текст, що займає більше місця у картці і тягне кнопку вниз</p>
<button class="ehd-btn">Детальніше</button>
</div>
<div class="ehd-card">
<p class="ehd-text">Середній текст</p>
<button class="ehd-btn">Детальніше</button>
</div>
</div>
<p class="case-label" style="margin-top: 0.75rem">✅ flex: 1 на тексті — кнопки внизу</p>
<div class="ehd-flex ehd-good">
<div class="ehd-card ehd-card--flex">
<p class="ehd-text ehd-text--grow">Коротко</p>
<button class="ehd-btn ehd-btn--green">Детальніше ✅</button>
</div>
<div class="ehd-card ehd-card--flex">
<p class="ehd-text ehd-text--grow">Значно довший текст, що займає більше місця у картці</p>
<button class="ehd-btn ehd-btn--green">Детальніше ✅</button>
</div>
<div class="ehd-card ehd-card--flex">
<p class="ehd-text ehd-text--grow">Середній текст</p>
<button class="ehd-btn ehd-btn--green">Детальніше ✅</button>
</div>
</div>
</div>
.equal-height-demo {
padding: 1rem; background: #f8fafc;
font-family: system-ui, sans-serif; font-size: 0.82rem;
}
.case-label { margin: 0 0 0.5rem; font-weight: 700; font-size: 0.78rem; }
.ehd-flex { display: flex; gap: 0.5rem; align-items: stretch; }
.ehd-card {
flex: 1;
background: white;
border: 1.5px solid #e2e8f0;
border-radius: 8px;
padding: 0.6rem;
}
.ehd-card--flex { display: flex; flex-direction: column; }
.ehd-text { margin: 0 0 0.5rem; line-height: 1.4; font-size: 0.78rem; color: #374151; }
.ehd-text--grow { flex: 1; margin-bottom: 0.5rem; }
.ehd-btn {
width: 100%; padding: 0.4rem;
border: 1.5px solid #d1d5db; border-radius: 6px;
background: white; cursor: pointer; font-size: 0.75rem;
font-family: inherit; color: #374151;
}
.ehd-btn--green { border-color: #86efac; background: #f0fdf4; color: #14532d; }
/* ✅ Рішення: flex column у картці + flex: 1 на тексті */
.card {
display: flex;
flex-direction: column;
}
.card__body {
flex: 1; /* займає весь вільний простір, тягне кнопку вниз */
}
.card__footer {
margin-top: auto; /* альтернативний варіант */
}
/* ✅ 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;
}
Не * { 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;
}
Ситуація: 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); }
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 без додаткових правил */
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/gridbox-sizing: border-box глобально💪 Flexbox
min-width: 0 на flex-items з текстомalign-items: flex-start якщо не потрібен stretchgap замість margin між flex-елементамиflex-wrap: wrap де потрібно📌 Позиціонування
position: relative на батьку для absolute дитиниoverflow: clip замість hidden якщо є stickyisolation: isolate для контролю stacking contextz-index — не більше 3–4 рівнів у проєкті🖼️ Зображення
max-width: 100%; height: auto; display: blockobject-fit: cover + aspect-ratio для рівної висотиloading="lazy" для off-screen зображеньwidth і height атрибути у HTML для CLS🔤 Текст
max-width: 65ch для читабельних рядківoverflow-wrap: break-word для довгих слів-webkit-line-clamp для multi-line truncate62.5% трюку на :root🏗️ Архітектура
@layer для керування пріоритетом!important у компонентахRendering Pipeline і CSS Performance
Як браузер перетворює CSS у пікселі: Critical Rendering Path, Layout → Paint → Composite, властивості що спричиняють reflow та repaint, GPU-прискорення через transform та opacity, will-change, content-visibility та CSS Containment.
Що таке Tailwind CSS і навіщо він потрібен
Глибоке занурення у філософію utility-first CSS. Чому Tailwind вирішує реальні проблеми CSS у великих проєктах, як він працює під капотом і коли його варто, а коли не варто використовувати.