Галоўная / Артыкулы

CSS адаптаваныя наследзіцтва ў час выконання: тэмавы керуванне, мосты JS і @property

Дазвольце дазнацься, які разліки ў кастамных атрыбутах CSS і зменных Sass, і як іх можна выкарыстоўваць для стварэння тэм, эфектаў, керованых JavaScript, гнеклівага шрыфту, варыянтав компанентоў і анімацый з пазначэнням типу.

Выдавец: REACTAPP.TOP17 хв чытання3986 слоў

Крыніцы: CSS Variables Are Underrated — Here’s What You’re Missing

Багатыя кодавескі зь іспользоўкай CSS-сваяйных атрыбутаў працуюць так сама, як і зменныя Sass: спіс кольцаў, выказаны ўсьмо толькі ў :root, і які больш ніколи не змянюецца. Гэта працуючы спосаб, але ён ігнаруе тое, што робіць іх разнымі. Сваяйные атрыбуты — це жывыя значэньні ў браузеры, якія наследуюцца через DOM, реагуюць на запыткі пра медыя і змены атрыбутаў, і можу быть апдэйтаваныя з JavaScript у будь-калі. Шырокі практычны адгук паказвае, што можна робіць за дапамой гэтага жывога поведзення — ад стварэння тэм і параметрызаваных компонентаў да анімацый з типамі за дапамой @property, а таксама выявляе механізмы і проблемы, якія ствараюць труднасці для команд.

Канстанты часу кампілявання проты жывых значэньняў

Прэпрасэсары такія як Sass, Less і Stylus гадзіны працавалі над стылюваннем фронтэнду за дапамою нясення, міксаў і змэнных. Калі ў браузерах з’явіліся власныя спецыяльныя атрыбуты, паўзучай рэакцыяю было тое, што яны нічога не дадаюць таго, чаго вже не прадставляў $primary-color. Гэтая думка стала прычыной ўсунення іх ад шырокага вжывання. Разлік крыўця ў тым, калі значэнне існуе:

+-------------------------------------------------------------------+
|                     SASS / SCSS VARIABLES                         |
|  Compile-Time Constants  -->  Evaluated Before Ship  -->  Static   |
+-------------------------------------------------------------------+

+-------------------------------------------------------------------+
|                    CSS CUSTOM PROPERTIES                          |
|  Runtime Live Properties --> Evaluated in Browser --> Dynamic     |
+-------------------------------------------------------------------+

Што стаёцца з прэпрасэсарскай змэнной

Змэнная Sass адначасова з’яўляецца ў рэзультате перад тым, як CSS дасягне браузера. У наступным файле SCSS другое значэнне прызначаецца ў блоке модифікатора dark:

$brand-color: #3b82f6;

.card {
  background-color: $brand-color;
}

.card--dark {
  $brand-color: #1e293b;
  background-color: $brand-color;
}

Кампайлер заменяе буквальнае значэнне там, дзе вжываецца змэнная, таму готовы файл стылю ўсё проста выглядае так:

.card {
  background-color: #3b82f6;
}

.card--dark {
  background-color: #1e293b;
}

Пасля компіляцыі імя $brand-color больш не існуе. Браузер не можа з’ясавіць, што обе раскліканні ўзятыя з адной зменны, і ён не можа зменіць гэтую значэнне у адпаведнасці да кліку, змены вікна або стану наведення. Кожная варыяцыя павінна быць створана як дадатковы CSS заздалегідь.

Што залишаецца ад спецыяльнай власнасці

Натыўная версія заставляе імя жывучым у браузеры:

:root {
  --brand-color: #3b82f6;
}

.card {
  background-color: var(--brand-color);
}

.card[data-theme="dark"] {
  --brand-color: #1e293b;
}

Хутчэй, --brand-color ўскладнівае вычыслены стыль кожнага элемента. Калі элемент .card атрымлея значэнне data-theme="dark", браузер перыякшае вычысленне var(--brand-color) для гэтага элемента і яго наследнікаў. Правіла для .card не дублююцца; вы проста зменяеце значэнне для адной галузі дрыва DOM. Корача кажучы: зменныя Sass заменяюцца пад час стварэння проекта і зникаюць пад час роботы, тады калі як стандартныя власнасці браузер вычысляе яны для кожнага элемента, яны беруць участак у процесе каскадавання стыліў і можу змяніцца пасля завантажэння сторонніцы.

Механізмы, якія трэба правільна разумець

Калькі правіла ўжо синтаксысу, дыямэнту і абрабоцкі каштоўных памылак можа адказаць на большасць неспадзяваных праблем, з якімі сталкаюцца разработчыкі.

Называнне і чытанне значэнняў

Назыв стандартной власнасці павінен начыняцца двумя галочкамі, а назывы ўсёвыразна залежныя ад роўню верхняй ліпкі, таму гэта две аднальныя власнасці:

:root {
  --main-color: #10b981;
  --Main-Color: #ef4444; /* Different property! */
}

Вы чытаете значэнне за дапамою функцыі var():

.button {
  background-color: var(--main-color);
}

Дыяхунак следуе за каскадам

Сваямыя атрыбуты павінуюцца звычайным правілам каскадаў і спадчынства. Якщо атрыбут задаць у :root, які адпавядае элементу <html>, ён становіцься доступным у всіх месцах. Якщо ж яго задаць у іншым селектары, ён заменіць попярэдні толькі для таго элемента і яго наследнікаў:

/* Global Scope */
:root {
  --spacing-unit: 1rem;
}

/* Sub-tree Scope Override */
.sidebar {
  --spacing-unit: 0.5rem; /* Overrides --spacing-unit for .sidebar and its descendants */
}

.sidebar .widget {
  padding: var(--spacing-unit); /* Computes to 0.5rem */
}

.main-content .widget {
  padding: var(--spacing-unit); /* Computes to 1rem */
}

Оба віджэты выкарыстоўваюць аднае і тое ж правіле, прычаму той, што знаходзіцца ў .sidebar, мае меншы абстаянне, таму што пераважае найбліжэйшая заданая значэнне. Саме гэта робіць перадзначэнняя ў межах дыяхунаку такімі простымі: вы зменяеце толькі значэнне, а не самае правіле.

Значэнні-замены

var() прыймае другі аргумент, які викорыстоўваецца, калі атрыбут, да якога ўжо існуе рэферэнс, не мае значэння. Значэнні-замены сабе можуць таксама містіць вызывы var(), таму ўмогацна ўтварыць ланцюгі такіх вызываў:

.avatar {
  /* Fallback to standard 40px if --avatar-size is undefined */
  width: var(--avatar-size, 40px);

  /* Nested fallbacks */
  color: var(--user-theme-color, var(--brand-default, #2563eb));
}

Паважайце тое, калі застаўляецца альтэрнатыўны варіант. Ён выкарыстоўваецца толькі тады, калі спецыяльная атрыбутнае значэнне не ўвядзена для таго элемента (і не успадкавана ад предка) або яе явна задана на initial. Якщо атрыбут ўвядзены, але мае значэнне, якое не мае сэнсу для цялевага элемента, напрыклад дыяметр у момент, калі апэкавана колер, альтэрнатыўны варіянт ігноруецца. Заўважыце, што unset не ёсць надзеяным спосабам яго актывацыі, таму што спецыяльныя атрыбуты за значэнчым правілам успадкаваны, так што unset працуе як inherit і бера значэнне ад родзіча, якое ёсць.

Недзейнае значэнне ў момент вычыслення

Звычная декларацыя з недзейным значэннем адхоўванаецца ў момент парсавання, таму ранейшая дэйная декларацыя застаецца чынной. Декларацыі, якія маюць var(), не можна пераканаць пад час парсавання, таму што заменяецца значэнне застаецца невядомым пакуль не пройдзе каскадаванне:

.box {
  color: red;
  color: var(--not-a-color, 12px); /* Invalid type for color! */
}

Парсер прыймае другі рядок з color і пераканае значэнне color: red. Ляшчэ толькі пад час вычыслення значэння браузер убачае, што 12px не ўжо кольор. У гэты момент паказаная раней декларацыя вже не мае значэння, таму атрыбут працюе так, як будзь-гэты unset: спадчыныя атрыбуты, такія як color, беруць значэнне ад бацькага элемента, а неспадчыныя — свае пачатковыя значэння (на прыклад, transparent для background-color). Гэта тыхае непрацяванне ўскладнення, якое прагчае рашыць механізм @property, пра які будзе сказана пазней.

Тэмавыя настройкі без дублювання правіл

Класычная реалізацыя темнага режыму дублюе кожнае правілу, якое вплывае на тэму:

/* Traditional approach: Duplicating declarations */
.card {
  background-color: #ffffff;
  color: #1e293b;
  border: 1px solid #e2e8f0;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}

body.dark-mode .card {
  background-color: #0f172a;
  color: #f8fafc;
  border: 1px solid #334155;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.5);
}

За дапамою калькі прынцыпоў гэта ўможна кантролюваць. Калі ж існуе дзесяткі компанентаў, кожны з якіх мае багато заданняў, прыналежных да певнай тэмы, кожная тэма збільшае колькасць селектараў, якія трэба синхронізаваць.

Токены робяць компаненты незалежнымі ад тэмы

Якщо рашэнняя з прыметаў дизайна знаходзяцца ў спецыяльных прынцыпоў, правілы компанентаў больш не залежнаць ад таго, якая тэма ўвімкнутая:

/* 1. Define Design Tokens */
:root {
  --bg-surface: #ffffff;
  --text-primary: #1e293b;
  --border-subtle: #e2e8f0;
  --shadow-alpha: 0.05;
}

[data-theme="dark"] {
  --bg-surface: #0f172a;
  --text-primary: #f8fafc;
  --border-subtle: #334155;
  --shadow-alpha: 0.5;
}

/* 2. Write Component CSS Once */
.card {
  background-color: var(--bg-surface);
  color: var(--text-primary);
  border: 1px solid var(--border-subtle);
  box-shadow: 0 4px 6px rgba(0, 0, 0, var(--shadow-alpha));
}

Правіле .card існуе толькі адна раз. Дааджэнне трэцяй тэмы, напрыклад [data-theme="high-contrast"], значыць неабходнасткую задаць чатыры значэння токанаў, замест таго каб перазадаваць правілы для кожнага компанента.

Стварэнне палітры з каналаў кольору

Можна пайшоць даўжэй, зберагаючы адзінкавыя каналы кольору, напрыклад у формате OKLCH, і вырахоўваючы з іх усі варіяцыі:

:root {
  /* Store raw OKLCH channels: Lightness, Chromaticity, Hue */
  --brand-l: 0.62;
  --brand-c: 0.19;
  --brand-h: 255;

  /* Base dynamic color */
  --brand-color: oklch(var(--brand-l) var(--brand-c) var(--brand-h));

  /* Automatically derived variations */
  --brand-hover: oklch(calc(var(--brand-l) - 0.1) var(--brand-c) var(--brand-h));
  --brand-active: oklch(calc(var(--brand-l) - 0.2) var(--brand-c) var(--brand-h));
  --brand-tint: oklch(0.95 calc(var(--brand-c) * 0.2) var(--brand-h));
  --brand-alpha-50: oklch(var(--brand-l) var(--brand-c) var(--brand-h) / 0.5);
}

.button-primary {
  background-color: var(--brand-color);
  color: white;
}

.button-primary:hover {
  background-color: var(--brand-hover);
}

.button-primary:active {
  background-color: var(--brand-active);
}

.badge {
  background-color: var(--brand-tint);
  color: var(--brand-color);
}

Варіанты «hover», «active», «tint» і «translucent» — усе гэта выражэнні, якія залежаць ад трох цыфр. Змена значэння --brand-h у элементе :root, чыраз працэю JavaScript або на падставе праганакоў пользователя, ўніверсальна зменяе базовы колер, яго статусы, ефект «tint» і додатковыя паверхні, без адной-лібо бібліятэкі для маніпуляцый з калерамі.

Для кожнага похадзячага токена існуе адна тонкасць: выраз var(), які знаходзится ў спецыяльным атрыбуте, вырахоўваецца на тым элементе, дзе гэты атрыбут заявлена, а ўсі наступныя элементы успадковуюць отрасленае значэнне. Паколькі --brand-color і падобныя значэння заявлены ў :root, перазначэнне --brand-h у глыбей структуры не вырахоўвае іх занова. Трэба альбо змяніць значэння ў :root, альбо перазаявіць похадзячыя токены ў тым пространстве імен, дзе вы перазначаеце значэння --brand-h.

Кераванне CSS з JavaScript

Адаптаваныя атрыбуты ўтвараюць чысты інтерфейс між скрыптамі та стылямі. У змену на напісанне строк сінтаксису стыляў адразу ў коде чы или кераванне комбінаціямі класаў, JavaScript можа перадаваць неапранутыя значэння, такія як координаты курсора чы стан прыемлена, да CSS і дазволіць файлу з правіламі стыляў самам вяршыць з імі.

Акцэнт, який следуе за курсорам

Пачніце з элемента card:

<div class="glow-card" id="card">
  <h3>Interactive Reactivity</h3>
  <p>Hover over this card to see real-time cursor lighting.</p>
</div>

Файл з правіламі стыляў задае стандартныя координаты і выкарыстоўвае іх для розмешчэння радыяльнага градіента ў псевдо-элементе:

.glow-card {
  --mouse-x: 50%;
  --mouse-y: 50%;

  position: relative;
  background-color: #1e1e2e;
  border-radius: 12px;
  padding: 2rem;
  color: #cdd6f4;
  overflow: hidden;
}

/* Radial light overlay following cursor coordinates */
.glow-card::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(
    800px circle at var(--mouse-x) var(--mouse-y),
    rgba(255, 255, 255, 0.08),
    transparent 40%
  );
  pointer-events: none;
}

Скрыпт толькі паведамляе, дзе знаходзіцца курсор, стосаваўшыся элемента card:

const card = document.getElementById('card');

card.addEventListener('pointermove', (e) => {
  const rect = card.getBoundingClientRect();
  const x = e.clientX - rect.left;
  const y = e.clientY - rect.top;

  // Send raw coordinates to CSS
  card.style.setProperty('--mouse-x', `${x}px`);
  card.style.setProperty('--mouse-y', `${y}px`);
});

Сама сутнасць гэтага раздзелу працы заключаецца ў тым, што JavaScript запісвае два числы на адны элемент і ніколі не чапаецца за сам градыент. Аднак гэта не безкоштовна: кожная апцэнза выклікае перысчытанне стылю карточкі, а чынаменна, калі градыент зміняецца, накладка паўтарна абрасовываецца. Гэтыя затраты незначныя для аднаго элемента, таму эфект зазвычай працюе плавна, але гэта не ўраджанне „з нуля“ на адной з вялікіх задач, якае выконваецца паўтарна.

Полоска прыбылі скролла

Тая ж ідея дазволяе перадаць пазіцыю скролла ў CSS як частку без единицы межаючуюся з 0 і 1:

window.addEventListener('scroll', () => {
  const scrollTop = window.scrollY;
  const docHeight = document.documentElement.scrollHeight - window.innerHeight;
  const scrollPercent = (scrollTop / docHeight);

  // Set unitless fraction (0 to 1)
  document.documentElement.style.setProperty('--scroll-progress', scrollPercent);
});

Полоска викорыстоўвае яе через transform, што запобегае выкананню задач з распалоžэнням элементаў, адтымушчы, што масштабаванне не зменяе размеру нічога іншага на сторанцы:

/* Progress indicator bar */
.scroll-progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  height: 4px;
  width: 100%;
  background-color: #3b82f6;
  transform-origin: left;
  /* Hardware-accelerated GPU transform using variable */
  transform: scaleX(var(--scroll-progress, 0));
  will-change: transform;
}

Ўсімае значэнне маюць два практычныя падыходы. Скрыпт запісвае данні ў document.documentElement, што являе сабо глобальную зміну, якая выконваецца часта, проты якой паветрабавае роздзіл пра выдатнасць; заданне значэння безпосередня ў самый стрэлку обмежвае перасчыт толькі аднаго элемента. А ў прыгледачах, якія падтрымляюць анімацыі, керованыя прасуванням, такія стрэлкі прогрэсу можна стварыць за дапамою animation-timeline: scroll() без якога-лібо JavaScript.

Гнекатыўная тайпаграфія і співвядношэння масштабавання

У тайпаграфіі, якая базуецца на пунктах перыходу, размеры зменяюцца між фіксаванымі значэннямі пры шырынах у розмере 768px і 1024px. Аднак аднароджэнне спецыяльных наявнасцей з clamp() і calc() дазволяе размерам зменяцца паступова між мінімумам і максімумам.

Расчыт клэмпа з параметрамі

Ідея заключаецца у тым, каб апісаць масштаб простымі цифрамі (дыапазон відображэння і дыапазон розмеру шрыфта) і вычысліць нахыл і точку перетнуя лінейнай функцыі межаў:

:root {
  /* Viewport and layout parameters */
  --fluid-min-width: 320;
  --fluid-max-width: 1200;

  /* Typography scale parameters */
  --font-min-size: 16;
  --font-max-size: 20;

  /* Compute slope and intersection for clamp() */
  --slope: (var(--font-max-size) - var(--font-min-size)) / (var(--fluid-max-width) - var(--fluid-min-width));
  --y-axis-intersection: -1 * var(--fluid-min-width) * var(--slope) + var(--font-min-size);

  /* Fluid base font size expression */
  --fluid-type-base: clamp(
    calc(var(--font-min-size) * 1px),
    calc(var(--y-axis-intersection) * 1px + (var(--slope) * 100vw)),
    calc(var(--font-max-size) * 1px)
  );
}

body {
  font-size: var(--fluid-type-base);
}

Спрыяйце гэтаму фрагменту як пачатковай точцы, а не як готоваму коду. Замена спецыяльных атрыбутаў вырабляецца у тэкстовай форме, таму калі --y-axis-intersection падставляецца ў calc(var(--y-axis-intersection) * 1px + ...), частка * 1px застосоўваецца толькі да последней часткі выразу. Першая частка, без единицы виміру, тады дадаецца да дужоў дзейства, што робіць усю функцыю clamp() некарэткай. Абгортаванне проміжных выразаў у кранікі, напрыклад --y-axis-intersection: (-1 * var(--fluid-min-width) * var(--slope) + var(--font-min-size));, або ў calc(), дапамагае зберагчы бажаную групавання.

Размеры загалоўкаў, вылічаныя на адной ступені

Калі є базовы размер, адзін каляр можа стварыць усю шкалу заголовакоў:

:root {
  /* Scale multiplier ratio (e.g., 1.25 Major Third) */
  --type-ratio: 1.25;

  --step-0: var(--fluid-type-base);
  --step-1: calc(var(--step-0) * var(--type-ratio));
  --step-2: calc(var(--step-1) * var(--type-ratio));
  --step-3: calc(var(--step-2) * var(--type-ratio));
  --step-4: calc(var(--step-3) * var(--type-ratio));
}

/* Application across elements */
p  { font-size: var(--step-0); }
h3 { font-size: var(--step-1); }
h2 { font-size: var(--step-2); }
h1 { font-size: var(--step-3); }

Кожны крок умножае пярэдні, таму прыцэпнасці межаў застаюцца аднаковымі. Змена каляра ў запите да медыя перасчыляе всі рэвэлі адразу:

@media (min-width: 1024px) {
  :root {
    /* Upgrade to 1.333 Perfect Fourth on large displays */
    --type-ratio: 1.333;
  }
}

Гэта працюе таму, што крокі і каляр адзначаюцца на тым жа элементе, :root, таму змены стануць видныя пад час вычыслення крокаў.

Компаненты з параметрамі заместо класаў-варіянтав

Класы-варіянты ў методалагіях BEM чы іншых часто прыводзяць да множэння: кожны размер, форма і колер становяць новы блак з адзначэннямі, які часткова павтарае базовы варыянт.

/* Traditional class bloat */
.btn { padding: 8px 16px; font-size: 14px; background: blue; border-radius: 4px; }
.btn--md { padding: 12px 24px; font-size: 16px; }
.btn--lg { padding: 16px 32px; font-size: 18px; }
.btn--rounded { border-radius: 9999px; }
.btn--danger { background: red; }
.btn--success { background: green; }

Адзіночныя власнасці як API компанента

Болей адаптаваны падход розглядае спецыяльныя атрыбуты як канфігурацыю компонента. Базовая правіла чытае кожную налагоджуемую значэння з атрыбута, які мае стандартнае значэння, а модифікаторы толькі зменяюць параметры:

.btn {
  /* 1. Component Defaults */
  --btn-bg: #3b82f6;
  --btn-color: #ffffff;
  --btn-px: 1.25rem;
  --btn-py: 0.625rem;
  --btn-radius: 0.375rem;
  --btn-font-size: 0.875rem;

  /* 2. Unified Rule Declaration */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--btn-py) var(--btn-px);
  background-color: var(--btn-bg);
  color: var(--btn-color);
  border-radius: var(--btn-radius);
  font-size: var(--btn-font-size);
  border: none;
  cursor: pointer;
  transition: background-color 0.2s, transform 0.1s;
}

.btn:hover {
  filter: brightness(0.9);
}

/* 3. Concise Declarative Modifiers */
.btn--lg {
  --btn-px: 1.75rem;
  --btn-py: 0.875rem;
  --btn-font-size: 1.125rem;
}

.btn--pill {
  --btn-radius: 9999px;
}

.btn--danger {
  --btn-bg: #ef4444;
}

.btn--ghost {
  --btn-bg: transparent;
  --btn-color: #3b82f6;
}

Модифікаторы, такія як .btn--lg, не павтараюць display, border-radius, cursor чы transition. Яны толькі задаюць новыя значэння для параметраў. Стайл-шіт застаецца маленькім, модифікаторы легка комбінуюцца (btn--lg btn--pill btn--danger проста работае), і калі варіант выглядае некоректна, вы знаёте, што прычына можа быць толькі ў калькі з некалькох можлівых варыянтав.

Тыпаваныя атрыбуты і анімацыя з @property

Незарэгістраваныя спецыяльныя атрыбуты маюць вядомая прыгожасць: іх нельзя выкарыстоўваць пад час пераходаў або стварэння кантрольных кадраў. Браузер спрыягае ўжо іх значэння як последовасці токенав без апаказу типу, таму ён не можа стварыць анімацыю з 0deg на 360deg або з red на blue; значэнне проста зменяецца ў певны момент часу анімацыі.

Правілле @property рэгіструе спецыяльны атрыбут з синтаксам, пачатковым значэнням і настройкам спадчынаў. Ёго падтрымліваюць у чынных версіях галоўных браузераў, але пераканайцеся ў падтрымцы для вашых цэляў пры яго выкарыстоўванні.

Рэгістрацыя атрыбутаў з апаказам типу

/* Register type-checked custom property */
@property --gradient-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@property --accent-color {
  syntax: "<color>";
  inherits: true;
  initial-value: #3b82f6;
}

Рэгістрацыя таксама рашае проблему тых самых таямнічых абяканняў: значэнне, якое не падходзіць па заявленаму синтаксаму, адхоўляецца, і атрыбут вяртаецца да свага зарэгістраванага пачатковага значэння, а не да неспадзяванага спадчыннага.

Пераходы на градіентах

Калі браузер з’ясавае, што --gradient-angle — це кут, а --accent-color — кольор, ён можа здзейсніць інтерполяцыю межы гэтых значэнняў, што дазволяе стварыць пераход на градіенте:

.animated-gradient-card {
  /* Reference the typed property inside radial/linear gradient */
  background: linear-gradient(
    var(--gradient-angle),
    #0f172a,
    var(--accent-color),
    #0f172a
  );

  border-radius: 16px;
  padding: 3rem;

  /* Animate custom property on hover! */
  transition: --gradient-angle 0.8s ease, --accent-color 0.5s ease;
}

.animated-gradient-card:hover {
  --gradient-angle: 180deg;
  --accent-color: #ec4899;
}

Непаўзліва анімазыя кадроў

Тая ж сама прыемка дазволяе выкорыстоўваць кадры для безпаўзлівага роцэння градіента:

@keyframes spin-gradient {
  to {
    --gradient-angle: 360deg;
  }
}

.glowing-border {
  animation: spin-gradient 4s linear infinite;
}

Памятайце, што анімазыя зарэгістраванай власнасці, якая выкорыстоўваецца ў градіенте, як і раней перамальоввае элемент у кожным кадре на галоўнай вясі; гэта не тое ж, што анімазыя transform чы opacity, якая выконваецца толькі на роўні композітара.

Прыдатнасц: Дзе вплываюць апдэты

Сваяе власнасці самыя па сабе не ўскладнююць роботу. Іх вартасць залежыць ад таго, якую частку дрэва даны апдэт узневажныць.

До якага розміру дасягае апдэт

Выклік element.style.setProperty('--name', value) спрычына невалідазацію стылю для таго элемента і для всьго, што можа яго успадкуваць:

+-------------------------------------------------------------------------+
|                  STYLE INVALIDATION CASCADE PATTERN                     |
|                                                                         |
|  [ :root Variable Mutation ]                                            |
|       └─> Invalidates Entire DOM Tree                                   |
|            └─> High Recalculation Cost (Large DOMs)                     |
|                                                                         |
|  [ Scoped Local Element Mutation ]                                      |
|       └─> Invalidates Only Local Component Branch                       |
|            └─> Minimal Engine Cost (60 FPS Safe)                        |
+-------------------------------------------------------------------------+
  • Змена атрыбута ў :root може змусіць браузер пераскладзіць стылі для кожнага элемента, який ад него залежыць у докумэнте. Выкананне гэтага пад кожным здарэнням scroll або mousemove на вялікай сторанцы можа спрычыніць затрэмтыванне экрана.
  • Установка атрыбута на конкрэты элемент, напрыклад, карточку, над якой знаходзіцца курсор, обмежвае пераскладзенне толькі гэтым роўнем.

Практычныя рэкамендаціі

Па-першыя, надаюць высокачастотныя апдэйты, такія як пазыцыі курсора або прагрэс скроллу, компаненту, які іх выкарыстоўвае, а не :root.

Друга рэч – для значэнняў, якія не патрэбны ніяму з наследнікаў, іх трэба зарэгістраваць з inherits: false, чыяму спосабам обмежваецца распространэнне змян пад час анімацыі:

@property --local-x {
  syntax: "<length>";
  inherits: false; /* Performance booster! Stops inheritance traversal */
  initial-value: 0px;
}

Є важлівая прычына: псэуда-элементы наследуюць атрыбуты з свага первіснага элемента, таму атрыбут, зарэгістраваны як ненаследуемы, установлены на элементе, не будзе видны ў ::before чы ::after гэтага элемента. Якщо псэуда-элемент чытае гэты значэння, трэба заставіць атрыбут наследуваны чы прыкласті эфект безпосередна да самага элемента.

Трэцяя рэч – калі значэнне вплывае на рух, яго трэба выкарыстоўваць через transform чы opacity, каб змяны не вплывалі на макетаванне і, як толькі можна, на атрыбут paint:

/* Performant GPU layer usage */
.box {
  transform: translate3d(var(--x, 0), var(--y, 0), 0);
}

Аднае ў аднам: карточка дашборда з тэмай

Наступны элемент канваса адаптаваець такія тэхнікі: два тэмы, свечанне, якае следзі за курсорам, контроль відтэнка, який змěняе калеры компонента, і гнучкія размеры заголовкаў. Маркап здзейснены за дапамою сэмантычнага элемента <article>, які складаецца з заголовка, кнопкі для увыключэння/увялічэння і поля вводу дыапазону:

<article class="dash-card" id="dashCard" data-theme="dark">
  <header class="dash-card__header">
    <span class="dash-card__badge">Live Metrics</span>
    <button class="dash-card__theme-btn" id="themeToggle">Toggle Theme</button>
  </header>

  <div class="dash-card__body">
    <h2 class="dash-card__title">CSS Variable Architecture</h2>
    <p class="dash-card__text">
      Hover over this component to trigger lighting updates. Use the color slider below to adjust primary hue channels across all child components in real time.
    </p>

    <div class="dash-card__controls">
      <label for="hueSlider">Primary Hue: <span id="hueVal">220</span>°</label>
      <input type="range" id="hueSlider" min="0" max="360" value="220">
    </div>
  </div>
</article>

Шаблон стылю адорганізаваны ў трох слояў: зарэгістраваныя атрыбуты і глобальныя токены, тэматычныя дыапазоны на карце, а таксама правілы компонента, якія чытаюць толькі токены:

/* ==========================================================================
   1. DESIGN TOKENS & SYSTEM SCALARS
   ========================================================================== */
@property --card-glow-x {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

@property --card-glow-y {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

:root {
  /* Default Color Channels */
  --hue: 220;
  --chroma: 0.18;

  /* Derived Dynamic Palette */
  --color-primary: oklch(0.60 var(--chroma) var(--hue));
  --color-primary-hover: oklch(0.50 var(--chroma) var(--hue));
  --color-primary-tint: oklch(0.95 calc(var(--chroma) * 0.3) var(--hue));

  /* Layout Dimensions */
  --radius-lg: 1rem;
  --radius-sm: 0.375rem;
}

/* ==========================================================================
   2. THEME SCOPES
   ========================================================================== */
.dash-card[data-theme="light"] {
  --bg-card: #ffffff;
  --bg-card-border: #e2e8f0;
  --text-main: #0f172a;
  --text-muted: #64748b;
  --glow-opacity: 0.12;
}

.dash-card[data-theme="dark"] {
  --bg-card: #0f172a;
  --bg-card-border: #1e293b;
  --text-main: #f8fafc;
  --text-muted: #94a3b8;
  --glow-opacity: 0.25;
}

/* ==========================================================================
   3. COMPONENT DECLARATION
   ========================================================================== */
.dash-card {
  position: relative;
  width: 100%;
  max-width: 480px;
  background-color: var(--bg-card);
  border: 1px solid var(--bg-card-border);
  border-radius: var(--radius-lg);
  padding: 2rem;
  color: var(--text-main);
  overflow: hidden;
  transition: background-color 0.3s, border-color 0.3s, color 0.3s;
  box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
}

/* Interactive Glow Accent Layer */
.dash-card::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(
    600px circle at var(--card-glow-x) var(--card-glow-y),
    oklch(0.70 var(--chroma) var(--hue) / var(--glow-opacity)),
    transparent 60%
  );
  pointer-events: none;
  transition: opacity 0.3s;
}

/* Header Elements */
.dash-card__header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 1.5rem;
}

.dash-card__badge {
  padding: 0.25rem 0.75rem;
  background-color: var(--color-primary-tint);
  color: var(--color-primary);
  border-radius: var(--radius-sm);
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.dash-card__theme-btn {
  background: transparent;
  border: 1px solid var(--bg-card-border);
  color: var(--text-main);
  padding: 0.5rem 0.875rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 0.875rem;
  transition: background-color 0.2s;
}

.dash-card__theme-btn:hover {
  background-color: var(--color-primary-tint);
}

/* Content Elements */
.dash-card__title {
  font-size: clamp(1.25rem, 2vw + 1rem, 1.75rem);
  margin-bottom: 0.75rem;
  color: var(--text-main);
}

.dash-card__text {
  font-size: 0.95rem;
  line-height: 1.6;
  color: var(--text-muted);
  margin-bottom: 1.5rem;
}

.dash-card__controls {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  background: oklch(0.50 var(--chroma) var(--hue) / 0.05);
  padding: 1rem;
  border-radius: var(--radius-sm);
}

.dash-card__controls label {
  font-size: 0.875rem;
  font-weight: 600;
}

Скрыпт паў’язуе элементы керавання з гэтымі токанамі, завжды запісваючы данні няў элементе карцы, а ў :root:

/* ==========================================================================
   4. REACTIVE SCRIPT BRIDGE
   ========================================================================== */
const card = document.getElementById('dashCard');
const themeBtn = document.getElementById('themeToggle');
const hueSlider = document.getElementById('hueSlider');
const hueVal = document.getElementById('hueVal');

// 1. Mouse coordinate tracking (Targeted scope prevents full layout invalidation)
card.addEventListener('pointermove', (e) => {
  const rect = card.getBoundingClientRect();
  card.style.setProperty('--card-glow-x', `${e.clientX - rect.left}px`);
  card.style.setProperty('--card-glow-y', `${e.clientY - rect.top}px`);
});

// 2. Interactive Theme Switcher
themeBtn.addEventListener('click', () => {
  const currentTheme = card.getAttribute('data-theme');
  const nextTheme = currentTheme === 'dark' ? 'light' : 'dark';
  card.setAttribute('data-theme', nextTheme);
});

// 3. Dynamic Color Engine Adjustment
hueSlider.addEventListener('input', (e) => {
  const newHue = e.target.value;
  hueVal.textContent = newHue;

  // Update hue variable locally on component container
  card.style.setProperty('--hue', newHue);
});

Перш чым выкласты такі компонент, пераканайцеся ў двух деталях у браузеры, таму што яны вынікаюць з вышэй описанай механікі:

  • Координаты свечэння заносяцца з параметрам inherits: false, але выкарыстоўваюцца ў элементе .dash-card::before. Паколькі псевдо-элемент не спадчыць іх, ён бачыць пачатковыя значэння 0px, і свечэнне застаецца ў канцы. Якщо заносіць іх з параметрам inherits: true або не заносіць ўзагалі, эфект будзе следаваць за курсорам.
  • Слайдер задае значэнне --hue для карткі, але --color-primary і --color-primary-tint вырахоўваюцца ў элементе :root, таму іконка застаецца ў своем первісным калеры. Рэагуюць толькі правілы, якія безпасэродна выкарыстоўваюць var(--hue) (свечэнне і фон кантролей). Перазадача вырахаванага палітры ў элементе .dash-card прымушае весь компонент следаваць за слайдерам.

Галоўныя вывары

  • Перакладзіце значэння, які разлічаюцца межы тэмамі і станамі, у спецыяльныя атрыбуты, і запісваце кожна правіла компонента толькі адназначы.
  • Актуальвайце токены на тым рэвэлі, дзе яны паўсходзяць — будзь то :root, рэгіон макета або адзін компонент, і дазвольце наследаванню перадаць іх да нижэй. Памятайце, што 파생аваныя токены вычысляюцца там, дзе яны былі актуальваны.
  • Дазвольце JavaScript перадаваць необработаныя значэння за дапамою element.style.setProperty() у замест на стварэнне стракаў стылю, і націльваецеся на самы маленькі элемент, які патрэбуе гэтага значэння.
  • Зарэгіструйце атрыбуты за дапамою @property і сінтаксісу на кшталт <color>, <angle> або <length>, каб атрымаць пераканальванне типу і павольнае інтэрполірацыя.
  • Зберагаеце каналы кольору ў формате HSL або OKLCH, каб ствараць відтэнкі, станы і прозрачнасць без викорыстоўвання бібліятэк.
  • Штодзенныя апдэйты трэба робіць не пад :root; для анімацый кращэ выкорыстоўваць transform і opacity, а inherits: false — толькі тады, калі жаданага значэння не патрабуе ніяны з наследзічных элементаў чы псевдо-элементаў.
  • Спецыяльныя атрыбуты — гэта не замена синтаксісу прэпрасэсара, а радыяй модель: стылі, якія ведаюць пра DOM, реагуюць на інтеракцыі і можна кераваць імі пад час выконання. Калі іх викорыстоўваць так, як описана, це дапамагае зменшыць размер дизайн-сістэм і спроставаць ўсунення змян.