Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «CSS змінні (користувацькі властивості)». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**CSS змінні** (custom properties) - це користувацькі властивості CSS з префіксом `--`, які зберігають значення, успадковуються по DOM-дереву і можуть змінюватися через JavaScript у рантаймі. ```css :root { --accent: #1e40af; } .btn { background: var(--accent); } .dark { --accent: #60a5fa; } /* перевизначення для нащадків */ ``` **Головне:** на відміну від Sass-змінних, вони не компілюються - живуть у DOM і реагують на JS-зміни.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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 виграє. Саме ця поведінка найчастіше дивує розробників, які стикаються з нею вперше.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.