In che modo Nordhealth utilizza proprietà personalizzate nei componenti web

I vantaggi dell'utilizzo delle proprietà personalizzate nei sistemi di progettazione e nelle librerie di componenti.

David Darnes
David Darnes

Sono Dave e lavoro come Senior Front-end Developer presso Nordhealth. Mi occupo di progettazione e sviluppo del nostro Design System Nord, che include la creazione di componenti web per la nostra libreria di componenti. Vorrei spiegarti come abbiamo risolto i problemi legati allo stile dei componenti web utilizzando le proprietà personalizzate CSS e alcuni degli altri vantaggi derivanti dall'utilizzo delle proprietà personalizzate nei sistemi di progettazione e nelle librerie dei componenti.

Come sviluppiamo i componenti web

Per creare i nostri componenti web utilizziamo Lit, una libreria che fornisce molto codice boilerplate, come stato, stili con ambito, modelli e altro ancora. Non solo è leggero, ma è anche basato sulle API JavaScript native, il che significa che possiamo fornire un bundle di codice che sfrutta le funzionalità già presenti nel browser.


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);
Un componente web scritto con Lit.

Ma l'aspetto più interessante dei componenti web è che funzionano con quasi tutti i framework JavaScript esistenti o persino con nessun framework. Una volta che nella pagina viene fatto riferimento al pacchetto JavaScript principale, l'utilizzo di un componente web è molto simile all'utilizzo di un elemento HTML nativo. L'unico vero segno che non si tratta di un elemento HTML nativo è il trattino coerente all'interno dei tag, che è uno standard per indicare al browser che si tratta di un componente web.


// TODO: DevSite - Code sample removed as it used inline event handlers
Utilizzare il componente web creato in precedenza in una pagina.

Incapsulamento in stile DOM shadow

Proprio come gli elementi HTML nativi hanno un DOM Shadow, così come i componenti web. Il DOM shadow è un albero nascosto di nodi all'interno di un elemento. Il modo migliore per visualizzarlo è aprire il controllo web e attivare l'opzione "Mostra albero DOM Shadow". Al termine dell'operazione, prova a esaminare un elemento di input nativo nella finestra di ispezione. A questo punto avrai la possibilità di aprire quell'input e vedere tutti gli elementi al suo interno. Potete anche provarlo con uno dei nostri componenti web: provate a esaminare il nostro componente di input personalizzato per vedere il suo DOM shadow.

Lo shadow DOM ispezionato in DevTools.
Esempio del DOM Shadow in un normale elemento di input di testo e nel nostro componente web di input Nord.

Uno dei vantaggi (o svantaggi, a seconda delle tue prospettive) di Shadow DOM è l'incapsulamento dello stile. Se scrivi codice CSS all'interno del tuo componente web, questi stili non possono trapelare e influire sulla pagina principale o su altri elementi e sono completamente contenuti all'interno del componente. Inoltre, il codice CSS scritto per la pagina principale o un componente web principale non possono infiltrarsi nel componente web.

Questo incapsulamento di stili è un vantaggio della nostra libreria di componenti. Questo ci dà una maggiore garanzia che, quando qualcuno utilizza uno dei nostri componenti, l'aspetto è quello previsto, indipendentemente dagli stili applicati alla pagina principale. Per essere sicuri, aggiungiamo all: unset; alla directory principale, o "host", di tutti i nostri componenti web.


:host {
  all: unset;
  display: block;
  box-sizing: border-box;
  text-align: start;
  /* ... */
}
Un codice boilerplate dei componenti che viene applicato alla radice shadow o al selettore host.

Tuttavia, cosa succede se un utente che utilizza il tuo componente web ha un motivo legittimo per cambiare alcuni stili? Magari c'è una riga di testo che necessita di maggiore contrasto a causa del suo contesto o un bordo deve essere più spesso? Se non è possibile inserire stili nel componente, come puoi sbloccare queste opzioni di stile?

È qui che entrano in gioco le proprietà personalizzate CSS.

Proprietà personalizzate CSS

Le proprietà personalizzate hanno un nome molto appropriato: sono proprietà CSS a cui puoi assegnare un nome completo e applicare qualsiasi valore necessario. L'unico requisito è aggiungere due trattini come prefisso. Dopo aver dichiarato la proprietà personalizzata, il valore può essere utilizzato nel CSS mediante la funzione var().


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

.n-color-accent-text {
  color: var(--n-color-accent);
}
Esempio dal nostro framework CSS di un token di design come proprietà personalizzata e di come viene utilizzato in una classe helper.

Quando si tratta di ereditarietà, vengono ereditate tutte le proprietà personalizzate, il che segue il comportamento tipico delle proprietà e dei valori CSS standard. Qualsiasi proprietà personalizzata applicata a un elemento principale o all'elemento stesso può essere utilizzata come valore su altre proprietà. Facciamo un uso intensivo delle proprietà personalizzate per i nostri token di progettazione applicandole all'elemento principale tramite il nostro CSS Framework, il che significa che tutti gli elementi della pagina possono utilizzare questi valori token, sia che si tratti di un componente web, di una classe helper CSS o di uno sviluppatore che vuole estrarre un valore dal nostro elenco di token.

Questa capacità di ereditare le proprietà personalizzate, con l'uso della funzione var(), consente di penetrare nel DOM shadow dei nostri componenti web e consente agli sviluppatori di avere un controllo più preciso durante lo stile dei componenti.

Proprietà personalizzate in un componente web del Nord

Ogni volta che sviluppiamo un componente per il nostro sistema di progettazione, adottiamo un approccio ponderato al CSS: vorremmo puntare a un codice leggero ma molto gestibile. I token di progettazione sono definiti come proprietà personalizzate all'interno del nostro framework CSS principale sull'elemento principale.


:root {
  --n-space-m: 16px;
  --n-space-l: 24px;
  /* ... */
  --n-color-background: rgb(255, 255, 255);
  --n-color-border: rgb(216, 222, 228);
  /* ... */
}
Le proprietà personalizzate CSS vengono definite nel selettore principale.

A questi valori dei token viene quindi fatto riferimento all'interno dei nostri componenti. In alcuni casi applicheremo il valore direttamente alla proprietà CSS, mentre in altri definiremo una nuova proprietà personalizzata contestuale a cui applicheremo il valore.


: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);
  /* ... */
}
Le proprietà personalizzate vengono definite nella radice shadow del componente e poi utilizzate negli stili del componente. Vengono utilizzate anche le proprietà personalizzate dell'elenco dei token di progettazione.

Astrarremo inoltre alcuni valori specifici del componente ma non presenti nei nostri token e li trasformeremo in una proprietà personalizzata contestuale. Le proprietà personalizzate contestuali al componente ci offrono due vantaggi principali. Innanzitutto, significa che possiamo "assorbire" meglio il nostro CSS, in quanto questo valore può essere applicato a più proprietà all'interno del componente.


.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);
}
La proprietà personalizzata contestuale della spaziatura interna dei gruppi di schede viene utilizzata in più punti nel codice del componente.

In secondo luogo, rende estremamente pulite le modifiche allo stato dei componenti e alle varianti: è solo la proprietà personalizzata che deve essere modificata per aggiornare tutte queste proprietà quando, ad esempio, stai applicando uno stato attivo o al passaggio del mouse oppure, in questo caso, una variante.


:host([padding="l"]) {
  --n-tab-group-padding: var(--n-space-l);
}
Una variante del componente della scheda in cui la spaziatura interna viene modificata mediante un singolo aggiornamento della proprietà personalizzata anziché più aggiornamenti.

Ma il vantaggio più importante è che, quando definiamo queste proprietà personalizzate contestuali su un componente, creiamo una sorta di API CSS personalizzata per ciascuno dei nostri componenti, che può essere utilizzata dall'utente di quel componente.


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

<style>
  nord-tab-group {
    --n-tab-group-padding: var(--n-space-xl);
  }
</style>
Utilizzare il componente Gruppo di schede nella pagina e aggiornare la proprietà personalizzata della spaziatura interna impostandola su una dimensione più grande.

L'esempio precedente mostra uno dei nostri componenti web con una proprietà personalizzata contestuale modificata tramite un selettore. Il risultato di questo intero approccio è un componente che offre all'utente una flessibilità di stile sufficiente, pur mantenendo sotto controllo la maggior parte degli stili effettivi. Inoltre, in qualità di sviluppatori di componenti, abbiamo la possibilità di intercettare gli stili applicati dall'utente. Se vogliamo modificare o estendere una di queste proprietà, possiamo farlo senza che l'utente debba modificare il codice.

Riteniamo che questo approccio sia estremamente efficace non solo per noi, in qualità di creatori dei componenti del nostro sistema di progettazione, ma anche per il nostro team di sviluppo quando li utilizza nei nostri prodotti.

Approfondire le proprietà personalizzate

Al momento della redazione del presente documento, non divulghiamo queste proprietà personalizzate contestuali nella nostra documentazione, ma intendiamo farlo in modo che il nostro team di sviluppo più ampio possa comprendere e sfruttare queste proprietà. I nostri componenti sono pacchettizzati in npm con un file manifest contenente tutte le informazioni necessarie. Utilizziamo quindi il file manifest come dati al momento del deployment del nostro sito di documentazione, operazione che avviene utilizzando Eleventy e la sua funzionalità Dati globali. Abbiamo intenzione di includere queste proprietà personalizzate contestuali in questo file di dati manifest.

Un'altra area che vogliamo migliorare è il modo in cui queste proprietà personalizzate contestuali ereditano i valori. Al momento, ad esempio, se vuoi regolare il colore di due componenti divisori, devi scegliere come target entrambi i componenti in modo specifico con i selettori o applicare la proprietà personalizzata direttamente all'elemento con l'attributo stile. Questo potrebbe sembrare corretto, ma sarebbe più utile se lo sviluppatore potesse definire questi stili su un elemento contenitore o anche a livello principale.


<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>
Due istanze del nostro componente divisore che richiedono due diversi trattamenti di colore. Uno è nidificato all'interno di una sezione che possiamo utilizzare per un selettore più specifico, ma dobbiamo scegliere come target il divisore in modo specifico.

Devi impostare il valore della proprietà personalizzata direttamente sul componente perché le definiamo nello stesso elemento tramite il selettore dell'host del componente. I token di progettazione globali che utilizziamo direttamente nel componente passano direttamente, non interessati da questo problema e possono anche essere intercettati sugli elementi principali. Come possiamo ottenere il meglio da entrambi i mondi?

Proprietà personalizzate private e pubbliche

Le proprietà personalizzate private sono state create da Lea Verou, ovvero una proprietà personalizzata "privata" contestuale sul componente stesso, ma impostata come proprietà personalizzata "pubblica" con un elemento di riserva.



: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);
  /* ... */
}
Il CSS del componente web divisore con le proprietà personalizzate contestuali regolate in modo che il CSS interno si basi su una proprietà personalizzata privata, che è stata impostata su una proprietà personalizzata pubblica con un elemento di riserva.

La definizione di proprietà personalizzate contestuali in questo modo significa che possiamo continuare a fare le cose che facevamo prima, ad esempio ereditare i valori token globali e riutilizzare i valori in tutto il codice del componente. Tuttavia, il componente erediterà anche le nuove definizioni della proprietà su se stesso o su qualsiasi elemento principale.


<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>
Di nuovo i due divisori, ma questa volta puoi ricolorare il divisore aggiungendo la proprietà personalizzata contestuale del divisore al selettore di sezione. Il divisore lo erediterà, producendo una porzione di codice più lineare e flessibile.

Anche se si può sostenere che questo metodo non è veramente "privato", riteniamo comunque che si tratti di una soluzione piuttosto elegante a un problema che ci preoccupava. Quando avremo l'opportunità, affronteremo questo problema nei nostri componenti in modo che il nostro team di sviluppo abbia un maggiore controllo sull'utilizzo dei componenti, beneficiando al contempo dei sistemi di protezione che abbiamo implementato.

Spero che queste informazioni su come utilizziamo i componenti web con le proprietà personalizzate CSS ti siano utili. Facci sapere la tua opinione e se decidi di usare uno di questi metodi nel tuo lavoro, puoi trovarmi su Twitter @DavidDarnes. Puoi trovare Nordhealth @NordhealthHQ su Twitter, nonché al resto del mio team, che si è impegnato duramente per unire questo sistema di progettazione ed eseguire le funzionalità menzionate in questo articolo: @Viljamis, @WickyNilliams e @eric_habich.

Immagine hero di Dan Cristian Pădureț