Strona główna / Artykuły

Własne właściwości CSS w czasie wykonywania: tematyzacja, mosty JS i @property

Dowiedz się, w jaki sposób własne właściwości CSS różnią się od zmiennych Sass oraz jak wykorzystywać je do tworzenia tematów, efektów sterowanych JavaScriptem, dynamicznego formatowania tekstu, wariantów komponentów oraz animacji typowanych.

Wydawca: REACTAPP.TOP17 min czytania3986 słów

Źródła: CSS Variables Are Underrated — Here’s What You’re Missing

Wiele baz kodu wykorzystuje własne właściwości CSS dokładnie tak samo jak zmienne Sass: listę kolorów deklarowaną raz w :root, której już nigdy nie zmienia się. To działa, ale ignoruje cechę, która sprawia, że są one różne. Własne właściwości to wartości dynamiczne w przeglądarce, które są dziedziczone przez DOM, reagują na zapytania mediów i zmiany atrybutów, a także mogą być aktualizowane z JavaScript w dowolnym momencie. Ten przewodnik pokazuje, co to zachowanie w czasie wykonywania umożliwia w praktyce – od tematyzacji i komponentów parametryzowanych po animacje typowane za pomocą @property – oraz wskazuje na mechanizmy i pułapki, które sprawiają problemy zespołom.

Stałe w czasie kompilacji a wartości dynamiczne

Preprocesory takie jak Sass, Less i Stylus przez lata kształtowały stylizację interfejsu użytkownika za pomocą nawijania, mixinów i zmiennych. Gdy w przeglądarkach pojawiły się wbudowane własne właściwości, powszechną reakcją było twierdzenie, że nie dodają one niczego, czego już nie oferował $primary-color. To założenie jest przyczyną ich niewystarczającego wykorzystania. Różnica polega na tym, kiedy wartość jest dostępna:

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

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

Co staje się zmienną preprocesora

Zmienna w Sass jest rozstrzygana jeszcze przed tym, jak CSS dotrze do przeglądarki. W poniższym SCSS druga wartość jest przypisywana wewnątrz bloku modyfikatora dark:

$brand-color: #3b82f6;

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

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

Kompilator zastępuje literową wartość wszędzie tam, gdzie używana jest zmienna, więc gotowa plik stylów wygląda po prostu tak:

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

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

Po skompilowaniu nazwa $brand-color już nie istnieje. Przeglądarka nie może wiedzieć, że obie kolory pochodzą z jednej zmiennej, ani nie może zmienić tej wartości w odpowiedzi na kliknięcie, zmianę rozdzielczości ekranu czy stan hover. Każda wariacja musi być wcześniej utworzona jako dodatkowy plik CSS.

Co pozostaje z własnej właściwości

Wersja natywna utrzymuje nazwę w przeglądarce:

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

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

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

Tutaj --brand-color jest częścią obliczonego stylu każdego elementu. Gdy .card otrzymuje wartość data-theme="dark", przeglądarz ponownie oblicza var(--brand-color) dla tego elementu oraz jego potomków. Reguła dotycząca .card nie jest kopiowana; zmienia się jedynie wartość dla jednej gałęzi drzewa DOM. Krótko mówiąc: zmienne Sass są zastępowane podczas procesu budowania i znikać po uruchomieniu strony, natomiast własne właściwości są rozstrzygane przez przeglądarz dla każdego elementu, biorą udział w mechanizmie kaskadowania i mogą ulegać zmianie po załadowaniu strony.

Mechanizmy, które musisz dobrze zrozumieć

Kilka zasad dotyczących składni, zakresu i obsługi błędów wyjaśnia większość zaskakujących zachowań, z którymi ludzie się spotykają.

Nazewnictwo i odczytywanie wartości

Nazwa własnej właściwości musi zaczynać się od dwóch myślników, a nazwy są wrażliwe na wielkość liter, więc są to dwie odrębne właściwości:

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

Czytasz wartość za pomocą funkcji var():

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

Zakres obowiązywania jest kaskadowy

Własne właściwości podlegają zwykłej kaskadowości i dziedziczeniu. Deklaracja właściwości w :root, który odpowiada elementowi <html>, sprawia, że jest ona dostępna wszędzie. Deklaracja w innym selektorze nadpisuje ją tylko dla tego elementu i jego potomków:

/* 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 */
}

Oba komponenty używają tej samej zasady, jednak ten znajdujący się w .sidebar ma mniejszą odległość między elementami, ponieważ zwycięża najbliższa deklaracja. To właśnie sprawia, że nadpisywanie w ramach określonego zakresu jest tak efektywne: zmienia się wartość, a nie zasadę.

Wartości awaryjne

var() przyjmuje drugi argument, który jest używany, gdy odwoływana właściwość nie ma wartości. Wartości awaryjne mogą same zawierać wywołania var(), więc można je łączyć w łańcuchy:

.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));
}

Należy być precyzyjnym co do momentu, w którym stosuje się rozwiązanie awaryjne. Jest ono używane tylko wtedy, gdy właściwość niestandardowa nie jest zdefiniowana dla danego elementu (ani nie została odziedziczona od przodka) lub została wyraźnie ustawiona na initial. Jeśli właściwość jest zdefiniowana, ale zawiera wartość, która nie ma sensu dla celu jej użycia – na przykład długość zamiast koloru – rozwiązanie awaryjne jest ignorowane. Należy pamiętać, że unset nie jest wiarygodnym sposobem na jego uruchomienie, ponieważ właściwości niestandardowe są domyślnie odziedziczane, więc unset zachowuje się jak inherit i przyjmuje wartość rodzica, jeśli taka istnieje.

Nieważne w momencie obliczania wartości

Zwykła deklaracja z nieważną wartością jest odrzucana podczas analizy, więc wcześniejsza ważna deklaracja nadal obowiązuje. Deklaracje zawierające var() nie mogą być sprawdzane podczas analizy, ponieważ zastąpiona wartość jest nieznana aż do wykonania procesu kaskadowego:

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

Parser przyjmuje drugi wiersz z wartością color i ją nadpisuje, zmieniając wartość na color: red. Dopiero podczas obliczania wartości komputer przeglądarki odkrywa, że 12px nie jest kolorem. W tym momencie wcześniejsza deklaracja już straciła na znaczeniu, więc właściwość zachowuje się tak, jakby była ustawiona na unset: właściwości odziedziczone, takie jak color, przyjmują wartość z elementu rodzicielskiego, natomiast te niewspółdzielone wracają do swojej początkowej wartości (na przykład transparent dla background-color). To ukryte niepowodzenie jest jednym z problemów, które ma rozwiązać @property, o którym będzie mowa później.

Tematyzacja bez powtarzających się reguł

Klasyczna implementacja trybu ciemnego powtarza każdą regułę tematyczną:

/* 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);
}

Z kilkoma właściwościami jest to do opanowania. Przy dziesiątkach komponentów, z których każdy ma wiele deklaracji tematycznych, każdy temat mnoży liczbę selektorów, które trzeba synchronizować.

Tokeny sprawiają, że komponenty są niezależne od tematu

Jeśli decyzje projektowe znajdują się w dostosowanych właściwościach, zasady komponentów przestają przejmować się tym, który temat jest aktywny:

/* 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));
}

Zasada .card istnieje tylko raz. Dodanie trzeciego tematu, na przykład [data-theme="high-contrast"], oznacza konieczność podania czterech wartości tokenów zamiast ponownego deklarowania zasad dla każdego komponentu.

Pobieranie palety z kanałów koloru

Można posunąć się dalej, przechowując poszczególne kanały koloru, na przykład w formacie OKLCH, i obliczając z nich wszystkie warianty:

: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);
}

Różne warianty takie jak hover, aktywny, z odcieniem i przezroczysty opierają się na trzech wartościach liczbowych. Zmiana wartości --brand-h w elementie :root, zarówno z poziomu JavaScript, jak i na podstawie preferencji użytkownika, spowoduje jednolite zmianę koloru bazowego, jego stanów, odcienia oraz warstw nakładanych, bez konieczności używania bibliotek do manipulacji kolorami.

Każdy pochodny token ma jedną subtelną cechę: wyrażenie var() umieszczone wewnątrz własności niestandardowej jest obliczane w elemencie, w którym ta właściwość została zadeklarowana, a elementy potomne odziedziczą uzyskaną wartość. Ponieważ --brand-color i podobne wartości są deklarowane w elemencie :root, zmiana tej wartości na głębszym poziomie drzewa nie powoduje ponownego obliczenia tych wartości. Należy albo zmienić wartość w :root, albo ponownie zadeklarować pochodne tokeny w obszarze, w którym zmienia się tę wartość.

Kierowanie CSS z JavaScript

Własne właściwości stanowią przejrzysty interfejs pomiędzy skryptami a stylami. Zamiast pisać łańcuchy stylów wewnątrz kodu lub manipulować kombinacjami klas, JavaScript może przekazać surowe wartości, takie jak współrzędne wskaźnika lub stan aplikacji, do CSS i pozwolić plikowi stylów zdecydować, co z nimi zrobić.

Akcentowanie podążające za kursorem

Zacznijmy od elementu typu card:

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

Plik stylów określa domyślne współrzędne i wykorzystuje je do umieszczenia gradientu promieniowego w pseudo-elementzie:

.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;
}

Skrypt jedynie informuje o tym, gdzie znajduje się wskaźnik względem elementu 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`);
});

Taka podział pracy jest atutem: JavaScript zapisuje dwa liczby na jednym elemencie i w ogóle nie ingeruje w sam gradient. Nie jest to jednak rozwiązanie bezkosztowe – każda aktualizacja powoduje ponowny obliczanie stylów karty, a ze względu na zmianę gradientu nakładka jest ponownie rysowana. Koszt ten jest niewielki przy pojedynczym elemencie, dlatego efekt zazwyczaj działa płynnie, ale nie jest to praca typu „zero reflow” wykonywana poza głównym wątkiem.

Pasek postępu przewijania

Ta sama idea pozwala przekazać pozycję przewijania do CSS jako ułamek bez jednostki w zakresie od 0 do 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);
});

Pasek wykorzystuje tę wartość za pomocą transform, co unika konieczności przeliczania układu, ponieważ skalowanie nie zmienia rozmiaru żadnych innych elementów na stronie:

/* 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;
}

Należy rozważyć dwie ulepszenia. Skrypt zapisuje dane do document.documentElement, co stanowi dokładnie tę globalną, częstą aktualizację, przed którą ostrzega sekcja dotycząca wydajności; ustawienie wartości bezpośrednio na samym pasku ogranicza ponowne obliczania do jednego elementu. Ponadto w przeglądarkach obsługujących animacje sterowane przewijaniem CSS, taki pasek postępu można stworzyć za pomocą animation-timeline: scroll() bez żadnego JavaScriptu.

Płynna typografia i proporcje skalowania

Typografia oparta na punktach przełączania zmienia rozmiary pomiędzy stałymi wartościami przy szerokościach takich jak 768px i 1024px. Połączenie własnych właściwości z funkcjami clamp() i calc() umożliwia natomiast ciągłą zmianę rozmiarów w zakresie od wartości minimalnej do maksymalnej.

Obliczenie za pomocą parametryzowanej funkcji clamp()

Celem jest opisanie skali za pomocą prostych liczb (zakres widoku i zakres rozmiaru czcionki) oraz wyznaczenie nachylenia i punktu przecięcia funkcji liniowej pomiędzy nimi:

: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);
}

Traktuj ten fragment jako punkt wyjścia, a nie gotowy kod. Podstawianie własnych właściwości odbywa się w formie tekstowej, więc gdy --y-axis-intersection zostanie wstawione do calc(var(--y-axis-intersection) * 1px + ...), wyrażenie * 1px odnosi się tylko do ostatniego składnika tej wyrażenia. Pierwszy, pozbawiony jednostki składnik jest następnie dodawany do długości, co sprawia, że całe wyrażenie clamp() staje się nieważne. Aby zachować zamierzone grupowanie, należy umieścić wyrażenia pośrednie w nawiasach, na przykład --y-axis-intersection: (-1 * var(--fluid-min-width) * var(--slope) + var(--font-min-size)); lub wewnątrz calc().

Rozmiary nagłówków harmonicznych na podstawie jednego stosunku

Gdy ustalono rozmiar bazowy, jedno stosunek może służyć do utworzenia całej skali nagłówków:

: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); }

Każdy krok mnoży poprzedni, dzięki czemu proporcje między poziomami pozostają spójne. Zmiana stosunku w zapytaniu mediowym przelicza wszystkie poziomy jednocześnie:

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

To działa, ponieważ kroki i stosunek są deklarowane w tym samym elemencie, :root, więc modyfikacja jest widoczna podczas obliczania kroków.

Komponenty z parametrami zamiast klas wariantowych

Klasy wariantowe w metodologii BEM lub podobnych sprawiają, że liczba deklaracji rośnie: każdy rozmiar, kształt i kolor stanowi kolejny blok deklaracji, który częściowo powtarza element bazowy.

/* 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; }

Własne właściwości jako API komponentu

Bardziej solidne podejście traktuje własności niestandardowe jako konfigurację komponentu. Podstawowa zasada polega na pobieraniu każdej wartości dostępnej do zmiany z właściwości posiadającej wartość domyślną, a modyfikatory jedynie zmieniają parametry:

.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;
}

Modyfikatory takie jak .btn--lg nie powtarzają wartości display, border-radius, cursor czy transition. Ustalają one jedynie nowe wartości dla tych parametrów. Skrypt stylizacji pozostaje niewielki, modyfikatory łączą się naturalnie (btn--lg btn--pill btn--danger po prostu działa), a gdy jakaś wersja wygląda niewłaściwie, wiadomo, że problem leży tylko w jednym z kilku takich elementów.

Własności typowane i animacje z użyciem @property

Niezarejestrowane własne właściwości mają dobrze znany ograniczenie: nie można ich wykorzystywać do interpolacji w przejściach ani klatkach kluczowych. Przeglądarka traktuje ich wartości jako sekwencje tokenów bez określonego typu, więc nie ma możliwości animacji od 0deg do 360deg lub od red do blue; wartość po prostu zmienia się w pewnym momencie podczas animacji.

Zasada @property rejestruje własną właściwość wraz z składnią, wartością początkową i ustawieniami dziedziczenia. Jest obsługiwana we współczesnych wersjach głównych przeglądarek, ale przed jej użyciem sprawdź dostępność w swoich celach.

Rejestrowanie właściwości typowanych

/* 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;
}

Rejestracja rozwiązuje również problem wcześniejszych niezauważonych błędów: wartość, która nie odpowiada deklarowanej składni, jest odrzucana, a właściwość przyjmuje zarejestrowaną wartość początkową zamiast nieoczekiwanej, odziedziczonej wartości.

Przejścia na gradientach

Gdy przeglądarka wie, że --gradient-angle to kąt, a --accent-color to kolor, może je interpolować, co umożliwia przejścia gradientowe:

.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;
}

Ciągła animacja kluczowych klatek

To samo zarejestrowanie umożliwia kluczowe klatki, które nieustannie obracają gradient:

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

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

Należy pamiętać, że animowanie zarejestrowanej właściwości używanej w gradientzie nadal powoduje ponowne narysowanie elementu na każdej klatce w głównej wątku; nie jest to takie samo jak animacja transform lub opacity realizowana wyłącznie przez komponentor.

Wydajność: Gdzie aktualizacje szkodzą

Swoje w sobie właściwości niestandardowe nie są wolne. Koszt zależy od tego, jak dużą część drzewa aktualizacja unieważnia.

Jak daleko sięga aktualizacja

Zawołanie element.style.setProperty('--name', value) powoduje unieważnienie stylów dla danego elementu oraz wszystkich elementów, które mogą od niego dziedziczyć:

+-------------------------------------------------------------------------+
|                  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)                        |
+-------------------------------------------------------------------------+
  • Zmiana właściwości w :root może zmusić przeglądarkę do ponownego obliczania stylów dla każdego elementu, który od niej zależy w całym dokumencie. Robienie tego przy każdym zdarzeniu scroll lub mousemove na dużej stronie może wpłynąć negatywnie na szybkość renderowania.
  • Ustawienie tej właściwości w konkretnym elemencie, np. w karcie, nad którą znajduje się wskaźnik, ogranicza ponowne obliczanie do tego konkretnego elementu.

Praktyczne wskazówki

Najpierw należy wysyłać aktualizacje o wysokiej częstotliwości, takie jak pozycje wskaźnika lub postęp przewijania, do komponentu, który je wykorzystuje, a nie do :root.

Z drugiej strony, dla wartości, których nie potrzebuje żaden potomek, zarejestruj je z użyciem inherits: false, co ogranicza rozprzestrzenianie się zmian podczas animacji:

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

Istnieje ważna uwaga: pseudo-elementy dziedziczą właściwości od swojego elementu źródłowego, więc zarejestrowana właściwość, która nie jest dziedziczona i została ustawiona na elemencie, nie będzie widoczna wewnątrz ::before lub ::after tego elementu. Jeśli pseudo-element odczytuje tę wartość, należy zachować dziedziczenie tej właściwości lub zastosować efekt bezpośrednio na elemencie.

Z trzeciej strony, gdy wartość wpływa na ruch elementu, wykorzystaj ją poprzez transform lub opacity, aby zmiana ominęła proces układania elementu oraz, jeśli to możliwe, jego rysowanie:

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

Łączenie elementów: karta dashboardu w temacie

Następujący widget łączy w sobie różne techniki: dwa tematy, efekt świetlisty podążający za kursorem, kontroler odcienia służący do zmiany koloru komponentu oraz dostosowywalne rozmiary nagłówków. Struktura kodu to semantyczny element <article> zawierający nagłówek, przycisk włączania/wyłączania oraz pole wprowadzania wartości:

<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>

Skrypt stylizacji jest zorganizowany w trzy warstwy: zarejestrowane właściwości i globalne tokeny, zakresy tematów na karcie oraz reguły komponentu, które jedynie odczytują te tokeny:

/* ==========================================================================
   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;
}

Skrypt łączy elementy sterujące z tymi tokenami, zawsze zapisując dane bezpośrednio na samej karcie, a nie w elemencie :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);
});

Zanim wyślesz taki komponent, sprawdź dwa szczegóły w przeglądarce, ponieważ oba wynikają z opisanej powyżej mechaniki:

  • Koordynaty światła są rejestrowane z ustawieniem inherits: false, ale są wykorzystywane w elementie .dash-card::before. Ponieważ pseudo-element nie dziedziczy tych wartości, widzi początkowe wartości 0px, przez co światło pozostaje w rogu. Rejestrowanie ich z ustawieniem inherits: true lub pozostawienie ich nierегистrowanych pozwala efektowi podążać za wskaźnikiem.
  • Suwak ustawia wartość --hue na karcie, ale --color-primary oraz --color-primary-tint są obliczane w elementie :root, więc naklejka zachowuje swój pierwotny kolor. Reagują jedynie reguły, które bezpośrednio używają var(--hue) (efekt światła oraz tło kontrol). Ponowne zadeklarowanie pochodnej palety kolorów w elemencie .dash-card sprawia, że cały komponent podąża za suwakiem.

Główne wnioski

  • Przenieś wartości, które różnią się między tematami i stanami, do niestandardowych właściwości i zapisz każdą regułę komponentu tylko raz.
  • Zadeklaruj tokeny na poziomie, do którego należą – czy to :root, obszar układu lub pojedynczy komponent – i pozwól dziedziczeniu przenieść je dalej. Pamiętaj, że tokeny pochodne są obliczane w miejscu ich deklaracji.
  • Zamiast budować łańcuchy stylów, pozwól JavaScriptowi przekazywać surowe wartości za pomocą element.style.setProperty() i skieruj się na najmniejszy element, który potrzebuje tej wartości.
  • Zarejestruj właściwości za pomocą @property oraz składni takiej jak <color>, <angle> lub <length>, aby uzyskać kontrolę nad typem danych i płynną interpolację.
  • Zachowaj kanały kolorów w formacie HSL lub OKLCH, aby bez użycia bibliotek tworzyć odcienie, stany i poziom przejrzystości.
  • Unikaj częstych aktualizacji w elemencie :root; dla efektów ruchu preferuj właściwości transform i opacity, a inherits: false używaj tylko wtedy, gdy żaden potomek ani pseudo-element nie wymaga tego wartości.
  • Właściwości niestandardowe nie zastępują składni preprocesora, lecz stanowią inny model: style, które znają strukturę DOM, reagują na interakcje i mogą być modyfikowane w czasie wykonywania. W ten sposób pozwalają one na stworzenie mniejszych systemów projektowych, które łatwiej jest modyfikować.