In che modo Nordhealth utilizza proprietà personalizzate nei componenti web

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

David Darnes
David Darnes

Sono Dave e sono Senior Front-end Developer di Nordhealth. Mi occupo della progettazione e dello 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 basati su ambito, modelli e altro ancora. Non solo è leggero, ma è anche basato su API JavaScript native, il che significa che siamo in grado di fornire un bundle di codice che sfrutta le funzionalità già disponibili 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 addirittura con nessun framework. Una volta che nella pagina si fa riferimento al pacchetto JavaScript principale, l'utilizzo di un componente web è molto simile all'utilizzo di un elemento HTML nativo. L'unico vero indicatore 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 dello stile DOM shadow

Proprio come gli elementi HTML nativi hanno un DOM ombra, lo stesso vale per i componenti web. Shadow DOM è un albero nascosto di nodi all'interno di un elemento. Il modo migliore per visualizzarlo è aprire l'ispettore web e attivare l'opzione "Mostra albero DOM ombra". Dopodiché, prova a esaminare un elemento di input nativo nell'inspector: ora avrai la possibilità di aprirlo e vedere tutti gli elementi al suo interno. Puoi anche provare questa operazione con uno dei nostri componenti web. Prova a ispezionare il nostro componente di input personalizzato per visualizzarne il DOM ombra.

Il DOM shadow ispezionato 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) dello 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 codice CSS scritto per la pagina principale o un componente web principale non può essere inserito nel componente web.

L'incapsulamento degli stili è un vantaggio della nostra libreria dei componenti. Ci offre una maggiore garanzia che, quando un utente utilizza uno dei nostri componenti, avrà l'aspetto che intendiamo, indipendentemente dagli stili applicati alla pagina principale. Per ulteriore 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 applicati alla radice shadow o al selettore host.

Tuttavia, cosa succede se un utente che utilizza il componente web dell'utente ha un motivo legittimo per modificare determinati stili? Forse c'è una riga di testo che richiede un maggiore contrasto a causa del 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à CSS personalizzate.

Proprietà personalizzate CSS

Le proprietà personalizzate hanno nomi molto appropriati: sono proprietà CSS a cui puoi assegnare un nome completo e che puoi applicare a qualsiasi valore necessario. L'unico requisito è che devi anteporre 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 dal nostro framework CSS di un token di progettazione come proprietà personalizzata e del suo utilizzo in una classe helper.

Quando si parla di ereditarietà, tutte le proprietà personalizzate vengono ereditate, in base al comportamento tipico delle proprietà e dei valori CSS normali. Qualsiasi proprietà personalizzata applicata a un elemento principale o all'elemento stesso può essere utilizzata come valore in altre proprietà. Facciamo un uso intensivo delle proprietà personalizzate per i nostri token di progettazione applicandoli all'elemento principale tramite il nostro framework CSS, il che significa che tutti gli elementi nella pagina possono utilizzare questi valori token, che si tratti di un componente web, una classe helper CSS o uno sviluppatore che vuole prendere un valore dal nostro elenco di token.

Questa capacità di ereditare le proprietà personalizzate, con l'uso della funzione var(), ci consente di accedere al DOM ombra dei componenti web e di offrire agli sviluppatori un controllo più granulare quando stilano i componenti.

Proprietà personalizzate in un componente web Nord

Ogni volta che sviluppiamo un componente per il nostro sistema di design, adottiamo un approccio ponderato al relativo CSS: ci piace puntare a un codice snello, ma molto manutenibile. I token di design di cui disponiamo 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);
  /* ... */
}
Definizione delle proprietà personalizzate CSS nel selettore principale.

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


: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 nell'elemento shadow del componente e poi utilizzate negli stili del componente. Vengono utilizzate anche le proprietà personalizzate elencate nell'elenco dei token di progettazione.

Inoltre, estraeremo 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 offrono due vantaggi chiave. Innanzitutto, significa che possiamo essere più "semplici" con il nostro CSS, in quanto il 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 che riempie il gruppo di schede viene utilizzata in più posizioni all'interno del codice del componente.

In secondo luogo, le modifiche allo stato del componente e alle varianti sono molto pulite: è solo la proprietà personalizzata che deve essere modificata per aggiornarle quando, ad esempio, stai definendo un passaggio del mouse o uno stato attivo o, in questo caso, una variante.


:host([padding="l"]) {
  --n-tab-group-padding: var(--n-space-l);
}
Una variante del componente della scheda in cui il padding viene modificato utilizzando 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 sfruttata 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>
Utilizza il componente gruppo di schede nella pagina e aggiorna la proprietà personalizzata di spaziatura interna su 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 sufficiente flessibilità per lo stile, mantenendo al contempo sotto controllo la maggior parte degli stili effettivi. Inoltre, come bonus, noi 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.

Questo approccio è estremamente efficace non solo per noi creatori dei componenti del sistema di progettazione, ma anche per il team di sviluppo che utilizza questi componenti nei nostri prodotti.

Espandere le proprietà personalizzate

Al momento della stesura di questo articolo, non mostriamo queste proprietà personalizzate contestuali nella nostra documentazione. Tuttavia, prevediamo di farlo in modo che il nostro team di sviluppo più grande 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 essi. Utilizziamo quindi il file manifest come dati quando viene eseguito il deployment del nostro sito di documentazione, utilizzando Eleventy e la relativa 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. Attualmente, ad esempio, se vuoi modificare 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 style. Potrebbe sembrare un problema, ma sarebbe più utile se lo sviluppatore potesse definire questi stili su un elemento contenitore o anche a livello della directory 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.

Il motivo per cui devi impostare il valore della proprietà personalizzata direttamente sul componente è che le stiamo definendo nello stesso elemento tramite il selettore dell'host del componente. I token di progettazione globali che utilizziamo direttamente nel componente passano direttamente, non sono interessati da questo problema e possono persino essere intercettati dagli elementi principali. Come possiamo ottenere il meglio da entrambi i mondi?

Proprietà personalizzate pubbliche e private

Le proprietà personalizzate private sono state create da Lea Verou, una proprietà personalizzata contestuale "privata" nel componente stesso, impostata su una 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 fallback.

Se definiamo le nostre proprietà personalizzate contestuali in questo modo, possiamo continuare a fare tutte le cose che facevamo prima, ad esempio ereditare i valori del token globale e riutilizzare i valori in tutto il codice del componente, ma il componente erediterà anche nuove definizioni di quella 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>
Ancora due separatori, ma questa volta il separatore può essere ricolorato aggiungendo la proprietà personalizzata contestuale del separatore al selettore della sezione. Il divisore lo erediterà, producendo una porzione di codice più pulita 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 ne 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 comunque dei sistemi di protezione messi in atto.

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

Immagine hero di Dan Cristian Pădureț