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

Mi chiamo Dave e sono Senior Front-end Developer presso Nordhealth. Mi occupo della progettazione e dello sviluppo del nostro sistema di progettazione Nord, che include la creazione di componenti web per la nostra libreria di componenti. Voglio condividere il modo in cui abbiamo risolto i problemi relativi allo stile dei componenti web utilizzando le proprietà personalizzate CSS e alcuni degli altri vantaggi dell'utilizzo delle proprietà personalizzate nei sistemi di progettazione e nelle librerie di componenti.

Come creiamo i componenti web

Per creare i nostri componenti web utilizziamo Lit, una libreria che fornisce un sacco di codice boilerplate come stato, stili con ambito, modelli e altro ancora. Lit non è solo leggero, ma è anche basato su API JavaScript native, il che significa che possiamo fornire un bundle di codice snello 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 la cosa più interessante dei componenti web è che funzionano con quasi tutti i framework JavaScript esistenti o anche senza framework. Una volta fatto riferimento al pacchetto JavaScript principale nella pagina, l'utilizzo di un componente web è molto simile all'utilizzo di un elemento HTML nativo. L'unico vero segno rivelatore 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.

Incapsulamento degli stili di Shadow DOM

Proprio come gli elementi HTML nativi hanno un shadow DOM, anche i componenti web ne hanno uno. Shadow DOM è un albero nascosto di nodi all'interno di un elemento. Il modo migliore per visualizzarlo è aprire l'ispezione web e attivare l'opzione "Mostra albero Shadow DOM". Una volta fatto, prova a esaminare un elemento di input nativo nell'inspector: ora avrai la possibilità di aprire l'input e visualizzare tutti gli elementi al suo interno. Puoi anche provare con uno dei nostri componenti web: prova a ispezionare il nostro componente di input personalizzato per visualizzare il relativo shadow DOM.

La shadow DOM esaminata in DevTools.
Esempio di shadow DOM in un normale elemento di input di testo e nel nostro componente web di input Nord.

Uno dei vantaggi (o svantaggi, a seconda del punto di vista) di Shadow DOM è l'incapsulamento degli stili. Se scrivi CSS all'interno del componente web, questi stili non possono fuoriuscire e influire sulla pagina principale o su altri elementi; sono completamente contenuti all'interno del componente. Inoltre, il CSS scritto per la pagina principale o un componente web principale non può essere inserito nel componente web.

Questo incapsulamento degli stili è un vantaggio nella nostra libreria di componenti. In questo modo abbiamo maggiori garanzie che quando qualcuno utilizza uno dei nostri componenti, questo avrà l'aspetto che abbiamo previsto, indipendentemente dagli stili applicati alla pagina principale. Per maggiore sicurezza, aggiungiamo all: unset; alla radice, o "host", di tutti i nostri componenti web.


:host {
  all: unset;
  display: block;
  box-sizing: border-box;
  text-align: start;
  /* ... */
}
Alcuni codici boilerplate dei componenti vengono applicati alla radice shadow o al selettore host.

Tuttavia, cosa succede se qualcuno che utilizza il tuo componente web ha un motivo legittimo per modificare determinati stili? Forse una riga di testo ha bisogno di un maggiore contrasto a causa del contesto o un bordo deve essere più spesso. Se nessuno stile può essere inserito nel componente, come puoi sbloccare queste opzioni di stile?

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

Proprietà personalizzate CSS

Le proprietà personalizzate sono denominate in modo molto appropriato: sono proprietà CSS a cui puoi assegnare il nome che preferisci e applicare il valore necessario. L'unico requisito è che siano preceduti da due trattini. Una volta dichiarata la proprietà personalizzata, il valore può essere utilizzato nel CSS utilizzando la funzione var().


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

.n-color-accent-text {
  color: var(--n-color-accent);
}
Esempio del nostro framework CSS di un token di progettazione come proprietà personalizzata e del suo utilizzo in una classe helper.

Per quanto riguarda l'ereditarietà, tutte le proprietà personalizzate vengono ereditate, il che segue il comportamento tipico delle proprietà e dei valori CSS regolari. Qualsiasi proprietà personalizzata applicata a un elemento principale o all'elemento stesso può essere utilizzata come valore in altre proprietà. Utilizziamo molto le proprietà personalizzate per i nostri token di progettazione applicandoli all'elemento radice tramite il nostro framework CSS, il che significa che tutti gli elementi della pagina possono utilizzare questi valori dei token, 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'utilizzo della funzione var(), ci consente di penetrare nel DOM ombra dei nostri componenti web e di consentire agli sviluppatori di avere un controllo più granulare quando applicano lo stile ai nostri componenti.

Proprietà personalizzate in un componente web Nord

Quando sviluppiamo un componente per il nostro sistema di progettazione, adottiamo un approccio ponderato al CSS: ci piace puntare a un codice snello ma molto manutenibile. I token di progettazione che abbiamo sono definiti come proprietà personalizzate all'interno del nostro framework CSS principale nell'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);
  /* ... */
}
Proprietà CSS personalizzate definite nel selettore principale.

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


: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);
  /* ... */
}
Proprietà personalizzate 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.

Inoltre, estrarremo 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, possiamo utilizzare un CSS più "asciutto", 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 del padding del gruppo di schede viene utilizzata in più punti del codice del componente.

In secondo luogo, rende le modifiche allo stato e alla variante dei componenti molto pulite: solo la proprietà personalizzata deve essere modificata per aggiornare tutte le proprietà quando, ad esempio, si applica uno stile a uno stato di passaggio del mouse o attivo o, in questo caso, a una variante.


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

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="T>itl<e"
  >!<-- ... --
/nord>-t<ab-gr>oup

style
  nord-tab-group {
    --n-tab-group-padding: var(--n-space<-xl);
>  }
/style
Utilizzando il componente del gruppo di schede nella pagina e aggiornando la proprietà personalizzata del padding a una dimensione maggiore.

L'esempio precedente mostra uno dei nostri componenti web con una proprietà personalizzata contestuale modificata tramite un selettore. Il risultato di questo approccio è un componente che offre all'utente una flessibilità di stile sufficiente, mantenendo comunque la maggior parte degli stili effettivi sotto controllo. Inoltre, come 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 utilizza questi componenti nei nostri prodotti.

Andare oltre con le proprietà personalizzate

Al momento della stesura, non riveliamo queste proprietà personalizzate contestuali nella nostra documentazione; tuttavia, prevediamo di farlo in modo che il nostro team di sviluppo più ampio possa comprendere e sfruttare queste proprietà. I nostri componenti sono pacchettizzati su npm con un file manifest, che contiene tutto ciò che c'è da sapere su di loro. Quindi utilizziamo il file manifest come dati quando viene implementato il nostro sito di documentazione, operazione eseguita utilizzando Eleventy e la relativa funzionalità Dati globali. Abbiamo intenzione di includere queste proprietà personalizzate contestuali in questo file di dati del manifest.

Un altro aspetto che vogliamo migliorare è il modo in cui queste proprietà personalizzate contestuali ereditano i valori. Attualmente, 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 di stile. Potrebbe sembrare una soluzione valida, ma sarebbe più utile se lo sviluppatore potesse definire questi stili in un elemento contenitore o anche a livello di radice.


<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 trattamenti di colore diversi. 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.

Il motivo per cui devi impostare il valore della proprietà personalizzata direttamente sul componente è che li definiamo sullo stesso elemento tramite il selettore host del componente. I token di progettazione globali che utilizziamo direttamente nel componente vengono passati direttamente, non sono interessati da questo problema e possono persino 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. Si tratta di una proprietà personalizzata "privata" contestuale sul componente stesso, ma impostata su una proprietà personalizzata "pubblica" con un fallback.



: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 è stato modificato in modo che il CSS interno si basi su una proprietà personalizzata privata, impostata su una proprietà personalizzata pubblica con un fallback.

Definire le nostre proprietà personalizzate contestuali in questo modo ci consente di continuare a fare tutto ciò che facevamo prima, ad esempio ereditare i valori dei token globali e riutilizzare i valori nel codice del componente. Tuttavia, il componente erediterà anche in modo controllato le nuove definizioni di questa 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 il divisore può essere ricolorato aggiungendo la proprietà personalizzata contestuale del divisore al selettore di sezione. Il divisore lo erediterà, producendo un codice più pulito e flessibile.

Sebbene si possa sostenere che questo metodo non sia veramente "privato", riteniamo comunque che sia 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 delle misure di salvaguardia in vigore.

Spero che questo approfondimento su come utilizziamo i componenti web con le proprietà personalizzate CSS ti sia stato utile. Fammi sapere cosa ne pensi e, se decidi di utilizzare uno di questi metodi nel tuo lavoro, puoi trovarmi su Twitter @DavidDarnes. Puoi trovare Nordhealth anche su Twitter all'indirizzo @NordhealthHQ, così come il resto del mio team, che ha lavorato duramente per riunire questo sistema di progettazione ed eseguire le funzionalità menzionate in questo articolo: @Viljamis, @WickyNilliams e @eric_habich.

Hero image di Dan Cristian Pădureț