Skip to main content

CSS змінні (користувацькі властивості)

CSS змінні (custom properties) - це користувацькі властивості CSS з префіксом --, які зберігають значення і розв'язуються браузером динамічно під час каскаду, як будь-яка інша властивість, з повним успадкуванням по DOM-дереву.

Теорія

TL;DR

  • CSS змінні - як спільний конфіг для стилів: визначаєш колір один раз у :root, кожен компонент читає звідти, і одна зміна оновлює все.
  • Головна різниця з Sass-змінними: Sass компілює їх у статичні значення ще до того, як браузер бачить файл. CSS змінні живуть у DOM і реагують на JS у рантаймі.
  • Використовуй, якщо значення повторюється в 3+ місцях або потрібне перемикання теми. Для разових значень - просто хардкодь.
  • Синтаксис: --назва: значення для визначення, var(--назва) для використання, var(--назва, fallback) для безпечного використання.
  • Область видимості - дерево DOM: дочірні елементи успадковують від батьківських, сусідні - ні.

Швидкий приклад

css
/* Визначаємо в :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-зміни тригерять часткове перерахування лише для властивостей, що залежать від зміненої змінної:

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

css
/* Неправильно */ .header { --color: blue; } .footer { color: var(--color); } /* Не розв'яжеться - .footer не всередині .header */ /* Правильно */ :root { --color: blue; }

Змінні існують у межах піддерева. Сусідні елементи не успадковують одне від одного.

Забувати fallback для відсутніх змінних

css
/* Неправильно - мовчки повертається до дефолту браузера */ color: var(--missing-color); /* Правильно */ color: var(--missing-color, #333);

Невалідне значення без fallback

css
:root { --size: red; } .box { width: var(--size); /* red невалідне для width - властивість ігнорується */ width: var(--size, 40px); /* fallback спрацьовує: беремо 40px */ }

Це ловить навіть досвідчених розробників. Специфікація каже: якщо підстановка дає невалідне значення для даної властивості, вона стає invalid at computed-value time. З fallback-ом - fallback виграє.

Намагатися використовувати змінні в умовах media queries

css
/* Не працює */ :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-атрибут

css
: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); }
js
function toggleTheme() { const html = document.documentElement; const current = html.getAttribute('data-theme'); html.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark'); }

Міняємо один атрибут на <html> і кожен компонент, що використовує токени, оновлюється. Жодних циклів по властивостях, жодних ре-рендерів.

Компонентні токени з calc()

css
: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 для відсутніх або невалідних значень

css
: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 виграє. Саме ця поведінка найчастіше дивує розробників, які стикаються з нею вперше.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?