Jak Nordhealth używa właściwości niestandardowych w komponentach internetowych

Zalety używania właściwości niestandardowych w systemach projektowania i bibliotekach komponentów.

David Darnes
David Darnes

Nazywam się Dave i jestem starszym programistą front-endu w firmie Nordhealth. Pracuję nad projektowaniem i rozwijaniem naszego systemu projektowania Nord, co obejmuje tworzenie komponentów internetowych na potrzeby naszej biblioteki komponentów. Chcę pokazać, jak rozwiązaliśmy problemy ze stylizowaniem komponentów internetowych za pomocą właściwości niestandardowych CSS, oraz przedstawić inne korzyści z używania właściwości niestandardowych w systemach projektowania i bibliotekach komponentów.

Jak tworzymy komponenty internetowe

Do tworzenia komponentów internetowych używamy Lit, biblioteki, która udostępnia wiele powtarzalnego kodu, takiego jak stan, style ograniczone, szablony i inne. Lit jest nie tylko lekki, ale też oparty na natywnych interfejsach API JavaScriptu, co oznacza, że możemy dostarczyć niewielki pakiet kodu, który wykorzystuje funkcje już dostępne w przeglądarce.


import {html, css, LitElement} from 'lit';

export class SimpleGreeting extends LitElement {
  static styles = css`:host { color: blue; font-family: sans-serif; }`;

  static properties = {
    name: {type: String},
  };

  constructor() {
    super();
    this.name = 'there';
  }

  render() {
    return html`

Hey ${this.name}, welcome to Web Components!

`
; } } customElements.define('simple-greeting', SimpleGreeting);
Komponent internetowy napisany za pomocą Lit.

Najbardziej atrakcyjną cechą komponentów internetowych jest jednak to, że działają z prawie każdą istniejącą platformą JavaScript, a nawet bez żadnej platformy. Gdy na stronie znajduje się odwołanie do głównego pakietu JavaScript, używanie komponentu internetowego jest bardzo podobne do używania natywnego elementu HTML. Jedynym znakiem, że nie jest to natywny element HTML, jest łącznik w tagach, który jest standardem informującym przeglądarkę, że jest to komponent internetowy.

Hermetyzacja stylu Shadow DOM

Podobnie jak natywne elementy HTML, komponenty internetowe mają Shadow DOM. Shadow DOM to ukryte drzewo węzłów w elemencie. Najlepszym sposobem na wizualizację tego jest otwarcie inspektora sieci i włączenie opcji „Pokaż drzewo Shadow DOM”. Gdy to zrobisz, spróbuj przyjrzeć się natywnemu elementowi wejściowemu w inspektorze – będziesz mieć teraz możliwość otwarcia tego elementu i zobaczenia wszystkich elementów wewnątrz niego. Możesz nawet wypróbować to z jednym z naszych komponentów internetowych – sprawdź nasz niestandardowy komponent wejściowy, aby zobaczyć jego model Shadow DOM.

Shadow DOM sprawdzony w Narzędziach deweloperskich.
Przykład modelu Shadow DOM w zwykłym elemencie do wprowadzania tekstu i w naszym komponencie internetowym Nord do wprowadzania danych.

Jedną z zalet (lub wad, w zależności od punktu widzenia) Shadow DOM jest enkapsulacja stylu. Jeśli napiszesz kod CSS w komponencie internetowym, te style nie będą mogły wyciekać i wpływać na stronę główną ani inne elementy. Są one całkowicie zawarte w komponencie. Dodatkowo kod CSS napisany dla strony głównej lub nadrzędnego komponentu internetowego nie może przenikać do komponentu internetowego.

Ta enkapsulacja stylów jest zaletą naszej biblioteki komponentów. Dzięki temu mamy większą pewność, że gdy ktoś użyje jednego z naszych komponentów, będzie on wyglądał tak, jak chcemy, niezależnie od stylów zastosowanych na stronie nadrzędnej. Aby mieć pewność, dodajemy all: unset; do katalogu głównego lub „hosta” wszystkich naszych komponentów internetowych.


:host {
  all: unset;
  display: block;
  box-sizing: border-box;
  text-align: start;
  /* ... */
}
Część powtarzalnego kodu komponentu jest stosowana do elementu shadow root lub selektora hosta.

Co jednak, jeśli ktoś, kto używa Twojego komponentu internetowego, ma uzasadniony powód, aby zmienić niektóre style? Może jest jakiś wiersz tekstu, który ze względu na kontekst wymaga większego kontrastu, albo obramowanie powinno być grubsze? Jeśli do komponentu nie można zastosować żadnych stylów, jak można odblokować te opcje stylizacji?

Właśnie dlatego powstały właściwości niestandardowe CSS.

Właściwości niestandardowe CSS

Właściwości niestandardowe mają bardzo trafną nazwę – są to właściwości CSS, które możesz nazwać, jak chcesz, i zastosować do nich dowolną wartość. Jedynym wymaganiem jest dodanie przed nimi 2 łączników. Po zadeklarowaniu właściwości niestandardowej możesz użyć jej wartości w kodzie CSS za pomocą funkcji var().


:root {
  --n-color-accent: rgb(53, 89, 199);
  /* ... */
}

.n-color-accent-text {
  color: var(--n-color-accent);
}
Przykład tokena projektowego jako właściwości niestandardowej i użycia go w klasie pomocniczej z naszego frameworka CSS.

Jeśli chodzi o dziedziczenie, wszystkie właściwości niestandardowe są dziedziczone, co jest typowym zachowaniem zwykłych właściwości i wartości CSS. Każda właściwość niestandardowa zastosowana do elementu nadrzędnego lub samego elementu może być używana jako wartość innych właściwości. W przypadku tokenów projektowych intensywnie korzystamy z właściwości niestandardowych, stosując je do elementu głównego za pomocą platformy CSS. Oznacza to, że wszystkie elementy na stronie mogą używać tych wartości tokenów, niezależnie od tego, czy jest to komponent internetowy, klasa pomocnicza CSS czy deweloper, który chce pobrać wartość z naszej listy tokenów.

Możliwość dziedziczenia właściwości niestandardowych za pomocą funkcji var() pozwala nam przenikać przez Shadow DOM naszych komponentów internetowych i zapewniać deweloperom bardziej szczegółową kontrolę nad stylizacją naszych komponentów.

Właściwości niestandardowe w komponencie internetowym Nord

Za każdym razem, gdy tworzymy komponent dla naszego systemu projektowania, dokładnie analizujemy jego kod CSS – staramy się, aby był on jak najprostszy, ale jednocześnie łatwy w utrzymaniu. Tokeny projektowe, które mamy, są zdefiniowane jako właściwości niestandardowe w naszej głównej platformie CSS w elemencie głównym.


:root {
  --n-space-m: 16px;
  --n-space-l: 24px;
  /* ... */
  --n-color-background: rgb(255, 255, 255);
  --n-color-border: rgb(216, 222, 228);
  /* ... */
}
Właściwości niestandardowe CSS zdefiniowane w selektorze głównym.

Wartości tych tokenów są następnie używane w naszych komponentach. W niektórych przypadkach zastosujemy wartość bezpośrednio we właściwości CSS, ale w innych zdefiniujemy nową kontekstową właściwość niestandardową i zastosujemy wartość do niej.


:host {
  --n-tab-group-padding: 0;
  --n-tab-list-background: var(--n-color-background);
  --n-tab-list-border: inset 0 -1px 0 0 var(--n-color-border);
  /* ... */
}

.n-tab-group-list {
  box-shadow: var(--n-tab-list-border);
  background-color: var(--n-tab-list-background);
  gap: var(--n-space-s);
  /* ... */
}
Właściwości niestandardowe są definiowane w elemencie shadow root komponentu, a następnie używane w stylach komponentu. Używane są też właściwości niestandardowe z listy tokenów projektowych.

Wyodrębnimy też niektóre wartości, które są specyficzne dla komponentu, ale nie występują w naszych tokenach, i przekształcimy je we właściwość niestandardową kontekstu. Właściwości niestandardowe, które są kontekstowe dla komponentu, zapewniają nam 2 główne korzyści. Po pierwsze, oznacza to, że możemy być bardziej „suchi” w naszym CSS, ponieważ tę wartość można zastosować do wielu właściwości w komponencie.


.n-tab-group-list::before {
  /* ... */
  padding-inline-start: var(--n-tab-group-padding);
}

.n-tab-group-list::after {
  /* ... */
  padding-inline-end: var(--n-tab-group-padding);
}
Niestandardowa właściwość kontekstowa dopełnienia grupy kart jest używana w wielu miejscach w kodzie komponentu.

Po drugie, dzięki temu zmiany stanu i odmiany komponentu są bardzo przejrzyste – wystarczy zmienić właściwość niestandardową, aby zaktualizować wszystkie te właściwości, gdy na przykład stylizujesz stan najechania kursorem lub aktywny albo, jak w tym przypadku, odmianę.


:host([padding="l"]) {
  --n-tab-group-padding: var(--n-space-l);
}
Wariant komponentu karty, w którym dopełnienie jest zmieniane za pomocą jednej aktualizacji właściwości niestandardowej, a nie wielu aktualizacji.

Największą zaletą jest jednak to, że gdy definiujemy te kontekstowe właściwości niestandardowe w komponencie, tworzymy dla każdego z nich coś w rodzaju niestandardowego interfejsu API CSS, z którego może korzystać użytkownik tego komponentu.


<nord-tab-group label="Title">
  <!-- ... -->
</nord-tab-group>

<style>
  nord-tab-group {
    --n-tab-group-padding: var(--n-space-xl);
  }
</style>
Użycie komponentu grupy kart na stronie i zwiększenie rozmiaru właściwości niestandardowej dopełnienia.

Powyższy przykład pokazuje jeden z naszych komponentów internetowych z kontekstową właściwością niestandardową zmienioną za pomocą selektora. Dzięki temu komponent zapewnia użytkownikowi wystarczającą elastyczność stylizacji, a jednocześnie pozwala zachować kontrolę nad większością rzeczywistych stylów. Dodatkowo jako deweloperzy komponentów możemy przechwytywać style zastosowane przez użytkownika. Jeśli chcemy dostosować lub rozszerzyć jedną z tych właściwości, możemy to zrobić bez konieczności wprowadzania zmian w kodzie przez użytkownika.

Uważamy, że to podejście jest niezwykle skuteczne nie tylko dla nas jako twórców komponentów systemu projektowania, ale też dla naszego zespołu programistów, gdy używają tych komponentów w naszych usługach.

Więcej informacji o właściwościach niestandardowych

W momencie pisania tego artykułu nie ujawniamy tych kontekstowych właściwości niestandardowych w naszej dokumentacji, ale planujemy to zrobić, aby nasz szerszy zespół programistów mógł je zrozumieć i wykorzystać. Nasze komponenty są spakowane w npm z plikiem manifestu, który zawiera wszystkie informacje na ich temat. Następnie wykorzystujemy plik manifestu jako dane podczas wdrażania witryny z dokumentacją, co odbywa się za pomocą Eleventy i funkcji Global Data. Planujemy uwzględnić te kontekstowe właściwości niestandardowe w tym pliku danych manifestu.

Kolejnym obszarem, który chcemy ulepszyć, jest sposób, w jaki te kontekstowe właściwości niestandardowe dziedziczą wartości. Jeśli na przykład chcesz dostosować kolor 2 komponentów rozdzielających, musisz kierować reklamy na oba te komponenty za pomocą selektorów lub zastosować właściwość niestandardową bezpośrednio do elementu z atrybutem stylu. Może się to wydawać w porządku, ale byłoby bardziej pomocne, gdyby deweloper mógł zdefiniować te style w elemencie nadrzędnym lub nawet na poziomie głównym.


<nord-divider></nord-divider>

<section>
  <nord-divider></nord-divider>
   <!-- ... -->
</section>

<style>
  nord-divider {
    --n-divider-color: var(--n-color-status-danger);
  }

  section {
    padding: var(--n-space-s);
    background: var(--n-color-surface-raised);
  }
  
  section nord-divider {
    --n-divider-color: var(--n-color-status-success);
  }
</style>
Dwa wystąpienia naszego komponentu separatora, które wymagają dwóch różnych kolorów. Jedna z nich jest zagnieżdżona w sekcji, którą możemy wykorzystać do utworzenia bardziej szczegółowego selektora, ale musimy kierować reklamy konkretnie na separator.

Wartość właściwości niestandardowej musisz ustawić bezpośrednio w komponencie, ponieważ definiujemy je w tym samym elemencie za pomocą selektora hosta komponentu. Globalne tokeny projektowe, których używamy bezpośrednio w komponencie, są przekazywane bezpośrednio, bez wpływu na ten problem, i mogą być nawet przechwytywane w elementach nadrzędnych. Jak możemy połączyć te 2 podejścia?

Prywatne i publiczne właściwości niestandardowe

Prywatne właściwości niestandardowe to rozwiązanie opracowane przez Leę Verou. Jest to kontekstowa „prywatna” właściwość niestandardowa w samym komponencie, ale ustawiona jako „publiczna” właściwość niestandardowa z wartością zastępczą.



:host {
  --_n-divider-color: var(--n-divider-color, var(--n-color-border));
  --_n-divider-size: var(--n-divider-size, 1px);
}

.n-divider {
  border-block-start: solid var(--_n-divider-size) var(--_n-divider-color);
  /* ... */
}
Kod CSS komponentu internetowego Divider z dostosowanymi kontekstowymi właściwościami niestandardowymi, dzięki czemu wewnętrzny kod CSS opiera się na prywatnej właściwości niestandardowej, która została ustawiona na publiczną właściwość niestandardową z wartością zastępczą.

Zdefiniowanie naszych kontekstowych właściwości niestandardowych w ten sposób oznacza, że nadal możemy robić wszystko to, co wcześniej, np. dziedziczyć wartości tokenów globalnych i ponownie używać wartości w kodzie komponentu. Komponent będzie jednak również płynnie dziedziczyć nowe definicje tej właściwości w sobie lub w dowolnym elemencie nadrzędnym.


<nord-divider></nord-divider>

<section>
  <nord-divider></nord-divider>
   <!-- ... -->
</section>

<style>
  nord-divider {
    --n-divider-color: var(--n-color-status-danger);
  }

  section {
    padding: var(--n-space-s);
    background: var(--n-color-surface-raised);
    --n-divider-color: var(--n-color-status-success);
  }
</style>
Ponownie 2 separatory, ale tym razem można zmienić ich kolor, dodając do selektora sekcji kontekstową właściwość niestandardową separatora. Divider odziedziczy go, co pozwoli uzyskać bardziej przejrzysty i elastyczny kod.

Można twierdzić, że ta metoda nie jest do końca „prywatna”, ale uważamy, że jest to dość eleganckie rozwiązanie problemu, który nas niepokoił. Gdy będziemy mieć taką możliwość, zajmiemy się tym w naszych komponentach, aby nasz zespół programistów miał większą kontrolę nad ich użyciem, a jednocześnie mógł korzystać z wprowadzonych przez nas zabezpieczeń.

Mamy nadzieję, że te informacje o tym, jak używamy komponentów internetowych z właściwościami niestandardowymi CSS, okażą się przydatne. Daj nam znać, co o tym myślisz. Jeśli zdecydujesz się użyć którejś z tych metod w swojej pracy, możesz mnie znaleźć na Twitterze: @DavidDarnes. Możesz też znaleźć Nordhealth na Twitterze: @NordhealthHQ, a także resztę mojego zespołu, który ciężko pracował nad stworzeniem tego systemu projektowania i wdrożeniem funkcji wymienionych w tym dokumencie: @Viljamis, @WickyNilliams i @eric_habich.