Zalety używania właściwości niestandardowych w systemach projektowania i bibliotekach komponentów.
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);
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.
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;
/* ... */
}
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);
}
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);
/* ... */
}
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);
/* ... */
}
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);
}
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);
}
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>
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>
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);
/* ... */
}
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>
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.