CSS змінні (користувацькі властивості)
CSS змінні (custom properties) - це користувацькі властивості CSS з префіксом --, які зберігають значення і розв'язуються браузером динамічно під час каскаду, як будь-яка інша властивість, з повним успадкуванням по DOM-дереву.
Теорія
TL;DR
- CSS змінні - як спільний конфіг для стилів: визначаєш колір один раз у
:root, кожен компонент читає звідти, і одна зміна оновлює все. - Головна різниця з Sass-змінними: Sass компілює їх у статичні значення ще до того, як браузер бачить файл. CSS змінні живуть у DOM і реагують на JS у рантаймі.
- Використовуй, якщо значення повторюється в 3+ місцях або потрібне перемикання теми. Для разових значень - просто хардкодь.
- Синтаксис:
--назва: значеннядля визначення,var(--назва)для використання,var(--назва, fallback)для безпечного використання. - Область видимості - дерево DOM: дочірні елементи успадковують від батьківських, сусідні - ні.
Швидкий приклад
/* Визначаємо в :root - доступно всім елементам */
:root {
--brand-blue: #1e40af;
--gap: 1rem;
}
/* Використовуємо де потрібно */
.primary-btn {
background: var(--brand-blue);
padding: var(--gap);
}
/* Перевизначаємо для темної теми - діє лише на нащадків */
.dark-theme {
--brand-blue: #60a5fa;
}
/* Усередині .dark-theme кнопки матимуть #60a5fa, зовні - #1e40af */Весь патерн ось такий. Визначай у :root, використовуй через var(), перевизначай у вужчому селекторі.
Каскад і успадкування
CSS змінні беруть участь у звичайному каскаді. Елемент отримує значення змінної від найближчого предка, який її визначив. Якщо .card оголошує --color: red, то .card-title всередині успадкує це значення без жодного додаткового правила.
Тому :root працює як глобальний рівень за замовчуванням: він на вершині DOM-дерева, і всі елементи успадковують від нього. Але змінні існують у межах піддерева елемента. Два сусідніх елементи не можуть ділити змінну, визначену на одному з них.
Специфічність працює так само, як із звичайними властивостями. Клас .dark-theme перевизначає --brand-blue, і всі нащадки одразу отримують нове значення. JS для цього не потрібен.
Коли використовувати
- Перемикання теми (світла/темна): описуєш токени в
:root, перемикаєш клас або атрибутdata-themeчерез JS. - Дизайн-токени:
--space-xs: 0.25rem,--space-sm: 0.5remв окремому файлі, посилання по всій кодовій базі. - Перевизначення на рівні компонента: задаєш змінну на корені компонента, нащадки успадковують автоматично.
calc()зі спільною базою:padding: calc(var(--gap) * 2)позбавляє від магічних чисел.- Пропускай для: разових значень або якщо потрібна підтримка IE11. IE11 не підтримує взагалі - там допоможе Sass або
postcss-css-variables.
Як браузер це обробляє
Браузер розв'язує custom properties під час фази style resolution, вже після того як каскад розставив пріоритети за специфічністю. Движок (Blink у Chrome, Stylo у Firefox) зберігає кожну змінну в computed style map елемента. Коли зустрічає var(--foo), підставляє успадковане або локально визначене значення. Жодної компіляції наперед.
JS-зміни тригерять часткове перерахування лише для властивостей, що залежать від зміненої змінної:
// Читаємо - повертає '' якщо не задано, не null
const val = getComputedStyle(el).getPropertyValue('--brand-blue');
// Пишемо на кореневий елемент
document.documentElement.style.setProperty('--brand-blue', '#60a5fa');
// З !important - перебиває авторські стилі
el.style.setProperty('--foo', 'red', 'important');Під час code review я регулярно бачу перевірку if (val === null), яка завжди проходить - бо незадана змінна повертає порожній рядок, а не null. Перевіряй через if (val.trim()) або строге порівняння з ''.
Типові помилки
Очікувати глобальну область видимості без :root
/* Неправильно */
.header { --color: blue; }
.footer { color: var(--color); } /* Не розв'яжеться - .footer не всередині .header */
/* Правильно */
:root { --color: blue; }Змінні існують у межах піддерева. Сусідні елементи не успадковують одне від одного.
Забувати fallback для відсутніх змінних
/* Неправильно - мовчки повертається до дефолту браузера */
color: var(--missing-color);
/* Правильно */
color: var(--missing-color, #333);Невалідне значення без fallback
:root { --size: red; }
.box {
width: var(--size); /* red невалідне для width - властивість ігнорується */
width: var(--size, 40px); /* fallback спрацьовує: беремо 40px */
}Це ловить навіть досвідчених розробників. Специфікація каже: якщо підстановка дає невалідне значення для даної властивості, вона стає invalid at computed-value time. З fallback-ом - fallback виграє.
Намагатися використовувати змінні в умовах media queries
/* Не працює */
:root { --bp-mobile: 768px; }
@media (max-width: var(--bp-mobile)) { ... } /* Невалідний синтаксис */Змінні працюють лише всередині блоків оголошень (declaration blocks), не в умовах медіа-запитів.
Де використовується в реальних проектах
- Tailwind CSS використовує
--tw-bg-opacity: 1як частину системи утилітарних класів для кольорів. - Bootstrap 5 має
--bs-primary: #0d6efdу кореневій області - звідти живляться стилі всіх компонентів. - Material UI використовує
--mui-palette-primary-mainдля токен-орієнтованого тематизування. - Chakra UI обмежує
--chakra-colors-blue-500областю свого Provider-компонента, а не:root.
Патерн скрізь однаковий: описуєш токени в одному місці, компоненти посилаються на них, хочеш змінити тему - міняєш токени.
Питання для співбесіди
Q: Як працює синтаксис fallback у var() і що відбувається з кількома комами?
A: var(--foo, red) використовує --foo, якщо воно валідне, інакше red. Синтаксис var(--foo, red, blue) технічно валідний, але все після першої коми - це одне fallback-значення. Тобто тут fallback - це red, blue як один токен, а не два окремі варіанти.
Q: Яка різниця між CSS змінними і Sass-змінними в каскаді?
A: Sass компілює свої змінні в статичні значення ще до того, як браузер парсить CSS. Браузер ніколи не бачить $color, лише #1e40af. CSS змінні розв'язуються в рантаймі для кожного елемента окремо, тому JS може їх змінювати і вони реагують на структуру DOM.
Q: Чи може CSS змінна посилатися на іншу змінну?
A: Так. --double: calc(var(--base) * 2) повністю підтримується і розв'язується на етапі обчислення. Кругові залежності виявляються і вважаються невалідними.
Q: Чому зміна CSS змінної через JS не оновлює елемент візуально?
A: Зазвичай це питання специфічності. Якщо inline-стиль або більш специфічний селектор задає властивість статичним значенням, зміна змінної не матиме ефекту на цю властивість. Відкрий DevTools, перевір computed styles і подивись, яке оголошення виграє.
Q: Що робить setProperty з 'important' третім аргументом?
A: el.style.setProperty('--foo', 'red', 'important') позначає змінну як !important, що перебиває авторські стилі. Корисно в рідкісних сценаріях налагодження, не для продакшену.
Приклади
Перемикання теми через data-атрибут
:root {
--bg: #ffffff;
--text: #111111;
--accent: #1e40af;
}
[data-theme="dark"] {
--bg: #111111;
--text: #f0f0f0;
--accent: #60a5fa;
}
body {
background-color: var(--bg);
color: var(--text);
transition: background-color 0.2s, color 0.2s;
}
.cta-btn {
background: var(--accent);
}function toggleTheme() {
const html = document.documentElement;
const current = html.getAttribute('data-theme');
html.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark');
}Міняємо один атрибут на <html> і кожен компонент, що використовує токени, оновлюється. Жодних циклів по властивостях, жодних ре-рендерів.
Компонентні токени з calc()
:root {
--base-space: 1rem;
}
.card {
--card-space: var(--base-space); /* успадковує від :root */
padding: var(--card-space);
gap: calc(var(--card-space) / 2);
}
.card--compact {
--card-space: 0.5rem; /* одне перевизначення - padding і gap автоматично підлаштовуються */
}Один токен контролює відразу кілька властивостей. Перевизначаєш у модифікаторі - весь компонент перераховується.
Fallback для відсутніх або невалідних значень
:root {
--size: 20px;
}
.element {
width: var(--size, 30px); /* --size існує: беремо 20px */
}
.broken {
--size: red; /* невалідне для width */
width: var(--size, 40px); /* невалідне пропускається, fallback 40px */
}
.missing {
/* --undefined-var ніде не задано в дереві */
width: var(--undefined-var, 50px); /* fallback 50px */
}Специфікація обробляє невалідну підстановку так само, як відсутню змінну: fallback виграє. Саме ця поведінка найчастіше дивує розробників, які стикаються з нею вперше.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.