Пошук уроків, статей та іншого контенту
Опануємо принципи CSS Architecture: ізоляцію стилів, контроль залежностей і масштабування кодової бази.
У невеликому проєкті CSS часто починається з простих селекторів:
button {
background: blue;
}
.title {
font-size: 32px;
}Поки сторінка одна, такі правила можуть працювати нормально. Але зі зростанням проєкту виникають типові проблеми:
зміна стилю однієї кнопки впливає на всі кнопки;
селектори стають надто специфічними;
правила дублюються в різних файлах;
незрозуміло, у якому порядку підключати стилі;
компоненти залежать від структури HTML навколо них;
видалення старого CSS стає ризикованим.
CSS Architecture — це набір принципів і домовленостей, які допомагають організувати CSS так, щоб його було легко:
читати;
змінювати;
повторно використовувати;
тестувати;
масштабувати разом із проєктом.
Архітектура CSS не є одним конкретним фреймворком. Це спосіб поділити стилі та контролювати їхні залежності.
Стилі компонента не повинні випадково змінювати інші частини сторінки.
Поганий приклад:
.card h2 {
color: #222;
}
.card a {
color: blue;
}Ці правила залежать від того, що всередині .card використані саме h2 та a. Якщо заголовок буде замінено на h3, стилі можуть зникнути.
Краще описувати роль елемента через клас:
.card__title {
color: #222;
}
.card__link {
color: blue;
}Тепер компонент залежить не від типу HTML-елемента, а від його призначення.
Специфічність визначає, яке CSS-правило матиме перевагу. Що складніший селектор, то важче його перевизначити.
Порівняйте:
/* Низька специфічність */
.card__title {
color: #222;
}
/* Вища специфічність */
.page .content .card h2.card__title {
color: #222;
}Перевагу варто надавати:
класам;
одному класу на селектор;
коротким селекторам;
мінімальному використанню вкладених селекторів.
Небажано будувати стилі на:
!important;
ID-селекторах;
глибокій вкладеності;
селекторах типів у складних комбінаціях.
Компонент має бути зрозумілим без вивчення всього HTML-документа.
Наприклад:
.card {
padding: 1.5rem;
}
.card__title {
margin: 0;
}Тут зрозуміло, що .card__title є частиною .card.
Натомість такий код створює приховану залежність:
.content > div > h2 {
margin-bottom: 1rem;
}Щоб зрозуміти правило, потрібно знати точну структуру контейнерів. Будь-яка зміна HTML може зламати стилі.
Клас компонента має відповідати за одну логічну роль.
Наприклад:
.button {
/* Базовий вигляд кнопки */
}
.button--primary {
/* Варіант кнопки */
}
.button--large {
/* Розмір кнопки */
}Краще комбінувати такі класи, ніж створювати велику кількість майже однакових класів:
<button class="button button--primary button--large">
Зберегти
</button>Один із практичних способів організації — поділ стилів на рівні з різними відповідальностями.
Базовий рівень містить правила для всього документа:
box-sizing;
базові стилі body;
типографіку;
стилі посилань;
нормалізацію поведінки елементів.
/* base.css */
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
color: #1f2937;
background: #f8fafc;
font-family: Arial, sans-serif;
line-height: 1.5;
}
a {
color: inherit;
}Базові стилі не повинні знати про конкретну картку, форму чи навігацію.
Layout-класи визначають розташування великих частин сторінки:
контейнер;
сітку;
відступи між секціями;
позиціонування блоків.
/* layout.css */
.container {
width: min(100% - 2rem, 72rem);
margin-inline: auto;
}
.page-grid {
display: grid;
gap: 1.5rem;
}Layout не має відповідати за внутрішній вигляд конкретної картки. Його завдання — визначити, де розташований компонент.
Компонент — самодостатня частина інтерфейсу:
картка;
кнопка;
поле форми;
повідомлення;
навігаційне меню.
/* components/card.css */
.card {
padding: 1.5rem;
border: 1px solid #dbe3ec;
border-radius: 0.75rem;
background: #ffffff;
}
.card__title {
margin: 0;
font-size: 1.25rem;
}
.card__text {
margin: 0.75rem 0 0;
color: #64748b;
}Компонент повинен мати зрозумілий публічний інтерфейс: основний клас, елементи та модифікатори.
Утиліта виконує одну маленьку універсальну дію:
/* utilities.css */
.u-hidden {
display: none !important;
}
.u-text-center {
text-align: center;
}Утиліти можуть бути корисними, але їх не варто перетворювати на заміну компонентам. Клас .u-text-center описує одну властивість, а клас .card описує повноцінний компонент.
BEM — це спосіб називати класи, щоб показати їхню структуру та призначення.
Основні частини:
Block — незалежний компонент;
Element — частина компонента;
Modifier — варіант або стан компонента.
Приклад:
<article class="card card--featured">
<h2 class="card__title">CSS Architecture</h2>
<p class="card__description">
Структурований підхід до масштабування стилів.
</p>
<a class="card__link" href="/courses">Переглянути курс</a>
</article>Відповідні частини назви:
.card — блок;
.card__title — елемент;
.card__description — елемент;
.card__link — елемент;
.card--featured — модифікатор.
Елемент належить безпосередньо блоку, а не іншому елементу.
Переважно:
<div class="card">
<h2 class="card__title">Заголовок</h2>
</div>Замість надмірно довгих назв:
<div class="card">
<div class="card__content">
<h2 class="card__content__title">Заголовок</h2>
</div>
</div>Якщо внутрішня частина є окремим самодостатнім компонентом, її можна зробити окремим блоком.
Каскад — це механізм, за яким браузер визначає фінальне значення властивості, якщо на один елемент діє кілька правил.
На результат впливають:
походження правила;
каскадні шари;
специфічність;
порядок появи правила.
@layer дозволяє явно визначити порядок між групами стилів:
@layer base, layout, components, utilities;
@layer base {
body {
margin: 0;
font-family: Arial, sans-serif;
}
}
@layer components {
.button {
padding: 0.75rem 1rem;
border: 0;
border-radius: 0.5rem;
background: #2563eb;
color: #ffffff;
}
}
@layer utilities {
.u-text-center {
text-align: center;
}
}Оголошення порядку на початку файлу робить залежності очевидними:
базові стилі завантажуються першими;
layout і компоненти мають вищий пріоритет;
утиліти можуть цілеспрямовано перевизначати компонент.
Це краще, ніж покладатися лише на випадковий порядок підключення файлів.
!important як основний механізм!important може бути виправданим для окремої утиліти або для подолання зовнішнього правила, але масове використання створює нові проблеми:
.card {
color: #222 !important;
}
.page .content .card {
color: #333 !important;
}Тепер для зміни кольору доведеться додавати ще специфічніші правила або нові !important.
Спочатку варто перевірити:
чи правильно розташований стиль у каскаді;
чи не завелика специфічність іншого селектора;
чи можна використати модифікатор;
чи потрібен взагалі окремий клас.
Компонент не повинен змінювати внутрішні елементи іншого компонента.
Небажаний приклад:
.card .button {
margin-top: 1rem;
}Тут стиль .card знає, що всередині використовується .button. Якщо кнопку буде замінено або її структура зміниться, логіка відступу зникне.
Краще керувати розкладкою на рівні батьківського контейнера:
.card-actions {
display: flex;
gap: 0.75rem;
margin-top: 1rem;
}HTML:
<article class="card">
<h2 class="card__title">Заголовок</h2>
<div class="card-actions">
<button class="button button--primary">Підтвердити</button>
<button class="button button--secondary">Скасувати</button>
</div>
</article>Тепер .card-actions відповідає за групування дій, а .button — лише за вигляд кнопки.
Не варто прив’язувати компонент до конкретного батьківського селектора:
.sidebar .card {
width: 300px;
}Це означає, що картка залежить від розташування в .sidebar.
Якщо розмір є варіантом картки, краще явно описати його:
.card--compact {
max-width: 20rem;
}HTML:
<article class="card card--compact">
<h2 class="card__title">Компактна картка</h2>
</article>Залежність стала явною: клас модифікатора повідомляє, чому картка має інший розмір.
Коли однакові значення повторюються в багатьох компонентах, їх можна винести у CSS custom properties.
:root {
--color-primary: #2563eb;
--color-text: #1f2937;
--color-muted: #64748b;
--radius-md: 0.75rem;
--space-md: 1rem;
}
.button {
border-radius: var(--radius-md);
background: var(--color-primary);
color: #ffffff;
}
.card {
padding: var(--space-md);
border-radius: var(--radius-md);
color: var(--color-text);
}
.card__description {
color: var(--color-muted);
}Переваги:
значення мають єдине джерело;
зміну візуального стилю можна виконати централізовано;
компоненти використовують узгоджені кольори та відступи;
назви змінних пояснюють призначення значень.
Важливо називати змінні за роллю, а не лише за конкретним значенням:
/* Зрозуміло, де використовується значення */
--color-text-muted: #64748b;
/* Менш інформативно */
--gray-500: #64748b;Нижче наведено самодостатній приклад сторінки. У ньому:
базові стилі відокремлені від layout;
картка оформлена як компонент;
варіант картки описаний модифікатором;
кнопка є незалежним компонентом;
відступ між кнопками належить контейнеру дій;
каскад організований через @layer.
<!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CSS Architecture</title>
<style>
@layer base, layout, components, utilities;
@layer base {
:root {
--color-background: #f8fafc;
--color-surface: #ffffff;
--color-text: #1f2937;
--color-muted: #64748b;
--color-border: #dbe3ec;
--color-primary: #2563eb;
--color-primary-dark: #1d4ed8;
--radius-md: 0.75rem;
--space-sm: 0.75rem;
--space-md: 1rem;
--space-lg: 1.5rem;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--color-background);
color: var(--color-text);
font-family: Arial, sans-serif;
line-height: 1.5;
}
button {
font: inherit;
}
}
@layer layout {
.container {
width: min(100% - 2rem, 60rem);
margin-inline: auto;
}
.page-header {
padding-block: 2rem;
}
.card-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: var(--space-lg);
padding-bottom: 2rem;
}
}
@layer components {
.card {
display: flex;
flex-direction: column;
padding: var(--space-lg);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface);
}
.card--featured {
border-color: var(--color-primary);
}
.card__title {
margin: 0;
font-size: 1.25rem;
}
.card__description {
margin: var(--space-sm) 0 0;
color: var(--color-muted);
}
.card__actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-sm);
margin-top: auto;
padding-top: var(--space-lg);
}
.button {
display: inline-block;
padding: 0.7rem 1rem;
border: 1px solid transparent;
border-radius: 0.5rem;
cursor: pointer;
}
.button--primary {
background: var(--color-primary);
color: #ffffff;
}
.button--primary:hover {
background: var(--color-primary-dark);
}
.button--secondary {
border-color: var(--color-border);
background: var(--color-surface);
color: var(--color-text);
}
}
@layer utilities {
.u-text-center {
text-align: center;
}
}
</style>
</head>
<body>
<header class="page-header">
<div class="container u-text-center">
<h1>Навчальні матеріали</h1>
<p>Приклад компонентної структури CSS.</p>
</div>
</header>
<main class="container">
<section class="card-list" aria-label="Матеріали">
<article class="card">
<h2 class="card__title">Основи каскаду</h2>
<p class="card__description">
Як браузер визначає фінальні стилі елемента.
</p>
<div class="card__actions">
<button class="button button--primary">Відкрити</button>
</div>
</article>
<article class="card card--featured">
<h2 class="card__title">CSS Architecture</h2>
<p class="card__description">
Принципи ізоляції та масштабування стилів.
</p>
<div class="card__actions">
<button class="button button--primary">Вивчати</button>
<button class="button button--secondary">Деталі</button>
</div>
</article>
</section>
</main>
</body>
</html>У реальному проєкті стилі можна розділити на файли за відповідальністю:
styles/
├── base.css
├── layout.css
├── components/
│ ├── button.css
│ └── card.css
├── utilities.css
└── main.cssmain.css може визначати порядок підключення:
@layer base, layout, components, utilities;
@import url("base.css") layer(base);
@import url("layout.css") layer(layout);
@import url("components/button.css") layer(components);
@import url("components/card.css") layer(components);
@import url("utilities.css") layer(utilities);Альтернативно файли можна об’єднувати під час збірки проєкту. Важливим є не конкретний інструмент, а передбачувана структура та порядок шарів.
Перед написанням CSS варто визначити:
Що є основним блоком?
Які елементи входять до нього?
Які варіанти або стани потрібні?
Які значення мають бути спільними змінними?
Чи залежить компонент від конкретного батьківського елемента?
Чи можна перемістити компонент в інше місце без переписування його стилів?
Наприклад, для повідомлення можна створити такий публічний інтерфейс:
<div class="notice notice--success">
<strong class="notice__title">Готово</strong>
<p class="notice__text">Дані успішно збережено.</p>
</div>.notice {
padding: 1rem;
border: 1px solid;
border-radius: 0.5rem;
}
.notice--success {
border-color: #86efac;
background: #f0fdf4;
}
.notice__title {
display: block;
}
.notice__text {
margin: 0.25rem 0 0;
}Компонент не залежить від того, чи розміщений він у main, aside або модальному вікні.
Необов’язково переписувати всю кодову базу одразу. Безпечніше рухатися поступово:
Знайти глобальні правила, які найчастіше спричиняють побічні ефекти.
Замінити селектори типів на класи там, де потрібна компонентна поведінка.
Прибрати надмірну вкладеність.
Виділити повторювані значення у custom properties.
Додати модифікатори замість копіювання блоків стилів.
Перевірити компонент у різних контейнерах.
Видалити старе правило лише після перевірки всіх місць його використання.
Під час рефакторингу корисно змінювати одну відповідальність за раз. Наприклад, спочатку ізолювати кнопку, а вже потім змінювати систему відступів.
h2 {
margin-bottom: 1rem;
}
button {
border-radius: 0.5rem;
}Такі правила впливають на всі заголовки та кнопки. Якщо стиль потрібен лише компоненту, використовуйте його клас:
.card__title {
margin: 0;
}
.button {
border-radius: 0.5rem;
}.page main .content .card .card__header h2 {
font-size: 1.5rem;
}Краще:
.card__title {
font-size: 1.5rem;
}.card {
width: 70%;
margin-left: 12rem;
position: absolute;
top: 4rem;
}Тут картка одночасно описує власний вигляд і конкретне розташування на сторінці. Розташування краще передати контейнеру або layout-класу.
.blue-button {
background: blue;
}
.green-button {
background: green;
}Краще мати базовий компонент і модифікатори:
.button {
padding: 0.75rem 1rem;
border-radius: 0.5rem;
}
.button--primary {
background: blue;
}
.button--success {
background: green;
}.red-box {}
.big-title {}
.left-column {}Такі назви швидко застарівають після зміни дизайну. Переважно називати класи за роллю:
.alert {}
.page-title {}
.sidebar {}Створювати один великий CSS-файл без поділу за відповідальністю.
Використовувати !important, щоб приховати проблеми з каскадом.
Прив’язувати компонент до конкретної HTML-структури.
Застосовувати глибокі селектори замість класів компонентів.
Змішувати в одному класі стилі компонента, layout і локальні винятки.
Дублювати однакові кольори, відступи та радіуси без потреби.
Називати класи лише за кольором або розміром.
Створювати модифікатор без базового класу, коли варіант залежить від базових стилів.
Змінювати внутрішні елементи одного компонента через селектор іншого компонента.
Додавати нові правила, не перевіряючи, чи не існує вже відповідний компонент або утиліта.
CSS Architecture допомагає зберігати контроль над стилями в міру зростання проєкту.
Основні правила:
ізолюйте компоненти через класи;
тримайте специфічність низькою;
розділяйте base, layout, components та utilities;
використовуйте явні назви блоків, елементів і модифікаторів;
не прив’язуйте компоненти до випадкової HTML-структури;
керуйте порядком стилів через передбачувану структуру або @layer;
виносьте повторювані значення у CSS custom properties;
розділяйте відповідальність між layout і компонентами;
використовуйте !important лише у виняткових ситуаціях;
додавайте нові правила так, щоб їх можна було легко видалити або замінити.