I vantaggi dell'utilizzo delle proprietà personalizzate nei sistemi di progettazione e nelle librerie di componenti.
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. Volevo 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 molto 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);
Ma l'aspetto più interessante dei componenti web è che funzionano con quasi tutti i framework JavaScript esistenti o anche senza alcun 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 del fatto 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 dello stile del DOM shadow
Proprio come gli elementi HTML nativi hanno uno Shadow DOM, anche i componenti web ne hanno uno. Il DOM shadow è un albero di nodi nascosto all'interno di un elemento. Il modo migliore per visualizzarlo è aprire l'inspector web e attivare l'opzione "Mostra l'albero shadow DOM". A questo punto, 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 a farlo con uno dei nostri componenti web: prova a ispezionare il nostro componente di input personalizzato per visualizzare il relativo Shadow DOM.
Uno dei vantaggi (o degli svantaggi, a seconda del punto di vista) di Shadow DOM è l'incapsulamento dello stile. Se scrivi CSS all'interno del tuo 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 per un componente web principale non può essere trasferito al tuo componente web.
Questo incapsulamento degli stili è un vantaggio nella nostra libreria di componenti. Ci dà una maggiore garanzia che quando qualcuno utilizza uno dei nostri componenti, questo apparirà come 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;
/* ... */
}
Tuttavia, cosa succede se un utente che utilizza il tuo componente web ha un motivo legittimo per modificare determinati stili? Forse c'è una riga di testo che richiede un contrasto maggiore a causa del contesto o un bordo che deve essere più spesso? Se nessuno stile può entrare 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 che puoi nominare interamente tu e applicare qualsiasi valore necessario. L'unico requisito è che siano preceduti da due trattini. Una volta dichiarata la proprietà personalizzata, il valore può essere utilizzato nel CSS tramite la funzione var().
:root {
--n-color-accent: rgb(53, 89, 199);
/* ... */
}
.n-color-accent-text {
color: var(--n-color-accent);
}
Per quanto riguarda l'ereditarietà, tutte le proprietà personalizzate vengono ereditate, 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 per altre proprietà. Utilizziamo molto le proprietà personalizzate per i nostri token di progettazione applicandole all'elemento root utilizzando 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 proprietà personalizzate, con l'uso della funzione var(), è il modo in cui penetriamo nello Shadow DOM dei nostri componenti web e consentiamo agli sviluppatori di avere un controllo più granulare durante lo stile dei nostri componenti.
Proprietà personalizzate in un componente web Nord
Ogni volta che sviluppiamo un componente per il nostro sistema di progettazione, adottiamo un approccio ponderato al relativo CSS: ci piace puntare a un codice snello, ma molto gestibile. I token di progettazione che abbiamo sono definiti come proprietà personalizzate all'interno del nostro framework CSS principale sull'elemento root.
:root {
--n-space-m: 16px;
--n-space-l: 24px;
/* ... */
--n-color-background: rgb(255, 255, 255);
--n-color-border: rgb(216, 222, 228);
/* ... */
}
Questi valori dei token vengono poi richiamati all'interno dei nostri componenti. In alcuni casi, applicheremo il valore direttamente alla proprietà CSS, ma in altri 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);
/* ... */
}
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 offrono due vantaggi principali. Innanzitutto, significa che possiamo essere più "dry" con il nostro CSS, poiché 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);
}
In secondo luogo, rende molto semplici le modifiche allo stato e alle varianti dei componenti: per aggiornare tutte queste proprietà, ad esempio quando si applica uno stile a uno stato attivo o al passaggio del mouse o, in questo caso, a una variante, è sufficiente modificare la proprietà personalizzata.
:host([padding="l"]) {
--n-tab-group-padding: var(--n-space-l);
}
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>
L'esempio precedente mostra uno dei nostri componenti web con una proprietà personalizzata contestuale modificata utilizzando un selettore. Il risultato di questo approccio è un componente che offre all'utente una flessibilità di stile sufficiente, mantenendo al contempo sotto controllo la maggior parte degli stili effettivi. 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.
Sfruttare al meglio le proprietà personalizzate
Al momento della stesura di questo articolo, 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 eseguito il deployment del nostro sito di documentazione, utilizzando Eleventy e la sua funzionalità Global Data. Prevediamo di includere queste proprietà personalizzate contestuali in questo file di dati del manifest.
Un'altra area che vogliamo migliorare è il modo in cui queste proprietà personalizzate contestuali ereditano i valori. Ora, 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 di 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>
Il motivo per cui devi impostare il valore della proprietà personalizzata direttamente sul componente è che li stiamo definendo sullo stesso elemento utilizzando il selettore host del componente. I token di progettazione globali che utilizziamo direttamente nel componente passano direttamente, senza essere 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 e sono proprietà personalizzate "private" contestuali sul componente stesso, ma impostate su una proprietà personalizzata "pubblica" con un valore 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);
/* ... */
}
Definendo le nostre proprietà personalizzate contestuali in questo modo, possiamo comunque fare tutto ciò che facevamo prima, come ereditare i valori dei token globali e riutilizzare i valori nel codice del componente, ma il componente erediterà anche correttamente le 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>
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, pur continuando a beneficiare delle misure di protezione che abbiamo messo in atto.
Spero che questo approfondimento su come utilizziamo i componenti web con le proprietà personalizzate CSS ti sia stato utile. Facci sapere cosa ne pensi e, se decidi di utilizzare uno di questi metodi nel tuo lavoro, puoi trovarmi su Twitter @DavidDarnes. Puoi anche trovare Nordhealth @NordhealthHQ su Twitter, così come il resto del mio team, che ha lavorato duramente per riunire questo sistema di progettazione e implementare le funzionalità menzionate in questo documento: @Viljamis, @WickyNilliams ed @eric_habich.