Stile globale e locale in Next.js

Pubblicato: 2022-03-10
Riassunto veloce ↬ Next.js ha opinioni forti su come organizzare JavaScript ma non CSS. Come possiamo sviluppare modelli che incoraggino le migliori pratiche CSS seguendo anche la logica del framework? La risposta è sorprendentemente semplice: scrivere CSS ben strutturati che bilanciano le preoccupazioni di stile globali e locali.

Ho avuto un'ottima esperienza con Next.js per gestire progetti front-end complessi. Next.js è ostinato su come organizzare il codice JavaScript, ma non ha opinioni integrate su come organizzare i CSS.

Dopo aver lavorato all'interno del framework, ho trovato una serie di modelli organizzativi che ritengo siano conformi alle filosofie guida di Next.js ed esercitino le migliori pratiche CSS. In questo articolo, costruiremo insieme un sito Web (un negozio di tè!) per dimostrare questi modelli.

Nota : probabilmente non avrai bisogno di una precedente esperienza con Next.js, anche se sarebbe bene avere una conoscenza di base di React ed essere aperti all'apprendimento di alcune nuove tecniche CSS.

Scrivere CSS "vecchio stile".

Quando esaminiamo per la prima volta Next.js, potremmo essere tentati di considerare l'utilizzo di una sorta di libreria CSS-in-JS. Sebbene possano esserci vantaggi a seconda del progetto, CSS-in-JS introduce molte considerazioni tecniche. Richiede l'utilizzo di una nuova libreria esterna, che si aggiunge alle dimensioni del pacchetto. CSS-in-JS può anche avere un impatto sulle prestazioni causando rendering aggiuntivi e dipendenze dallo stato globale.

Lettura consigliata : " I costi invisibili delle prestazioni delle moderne librerie CSS-in-JS nelle app React)" di Aggelos Arvanitakis

Inoltre, lo scopo principale dell'utilizzo di una libreria come Next.js è di eseguire il rendering statico delle risorse quando possibile, quindi non ha molto senso scrivere JS che deve essere eseguito nel browser per generare CSS.

Ci sono un paio di domande che dobbiamo considerare quando si organizza lo stile all'interno di Next.js:

Come possiamo adattarci alle convenzioni/migliori pratiche del framework?

Come possiamo bilanciare le preoccupazioni di stile "globale" (caratteri, colori, layout principali e così via) con quelle "locali" (stili relativi ai singoli componenti)?

La risposta che ho trovato per la prima domanda è semplicemente scrivere un buon vecchio CSS . Next.js non solo supporta questa operazione senza alcuna configurazione aggiuntiva; produce anche risultati performanti e statici.

Per risolvere il secondo problema, adotto un approccio che può essere riassunto in quattro parti:

  1. Gettoni di design
  2. Stili globali
  3. Classi di utilità
  4. Stili dei componenti

Sono in debito con l'idea di Andy Bell di CUBE CSS ("Composizione, Utilità, Blocco, Eccezione") qui. Se non hai mai sentito parlare di questo principio organizzativo, ti consiglio di dare un'occhiata al suo sito ufficiale o alla sua funzione su Smashing Podcast. Uno dei principi che prenderemo da CUBE CSS è l'idea che dovremmo abbracciare piuttosto che temere la cascata CSS. Impariamo queste tecniche applicandole a un progetto di sito web.

Iniziare

Costruiremo un negozio di tè perché, beh, il tè è gustoso. Inizieremo eseguendo yarn create next-app per creare un nuovo progetto Next.js. Quindi, rimuoveremo tutto nella styles/ directory (è tutto il codice di esempio).

Nota : se vuoi seguire il progetto finito, puoi verificarlo qui.

Gettoni di progettazione

Praticamente in qualsiasi configurazione CSS, c'è un chiaro vantaggio nel memorizzare tutti i valori condivisi a livello globale nelle variabili . Se un cliente chiede un colore da cambiare, l'implementazione della modifica è un'unica battuta piuttosto che un enorme pasticcio di trova e sostituisci. Di conseguenza, una parte fondamentale della nostra configurazione CSS di Next.js sarà la memorizzazione di tutti i valori a livello di sito come token di progettazione .

Utilizzeremo le proprietà personalizzate CSS integrate per archiviare questi token. (Se non hai familiarità con questa sintassi, puoi consultare "A Strategy Guide To CSS Custom Properties".) Dovrei menzionare che (in alcuni progetti) ho scelto di utilizzare le variabili SASS/SCSS per questo scopo. Non ho trovato alcun vantaggio reale, quindi di solito includo SASS in un progetto solo se trovo che ho bisogno di altre funzionalità SASS (mix-in, iterazione, importazione di file e così via). Le proprietà personalizzate CSS, al contrario, funzionano anche con la cascata e possono essere modificate nel tempo anziché essere compilate staticamente. Quindi, per oggi, rimaniamo con i semplici CSS .

Nella nostra directory styles/ , creiamo un nuovo file design_tokens.css :

 :root { --green: #3FE79E; --dark: #0F0235; --off-white: #F5F5F3; --space-sm: 0.5rem; --space-md: 1rem; --space-lg: 1.5rem; --font-size-sm: 0.5rem; --font-size-md: 1rem; --font-size-lg: 2rem; }

Naturalmente, questo elenco può e crescerà nel tempo. Una volta aggiunto questo file, dobbiamo passare al nostro file pages/_app.jsx , che è il layout principale per tutte le nostre pagine, e aggiungere:

 import '../styles/design_tokens.css'

Mi piace pensare ai design token come il collante che mantiene la coerenza nel progetto. Faremo riferimento a queste variabili su scala globale, nonché all'interno dei singoli componenti, garantendo un linguaggio di progettazione unificato.

Altro dopo il salto! Continua a leggere sotto ↓

Stili globali

Successivamente, aggiungiamo una pagina al nostro sito Web! Passiamo al file pages/index.jsx (questa è la nostra homepage). Elimineremo tutto il boilerplate e aggiungeremo qualcosa come:

 export default function Home() { return <main> <h1>Soothing Teas</h1> <p>Welcome to our wonderful tea shop.</p> <p>We have been open since 1987 and serve customers with hand-picked oolong teas.</p> </main> }

Sfortunatamente, sembrerà abbastanza semplice, quindi impostiamo alcuni stili globali per gli elementi di base , ad esempio i tag <h1> . (Mi piace pensare a questi stili come a "predefiniti globali ragionevoli".) Possiamo sovrascriverli in casi specifici, ma sono una buona ipotesi su cosa vorremmo se non lo facessimo.

Lo metterò nel file styles/globals.css (che viene fornito per impostazione predefinita da Next.js):

 *, *::before, *::after { box-sizing: border-box; } body { color: var(--off-white); background-color: var(--dark); } h1 { color: var(--green); font-size: var(--font-size-lg); } p { font-size: var(--font-size-md); } p, article, section { line-height: 1.5; } :focus { outline: 0.15rem dashed var(--off-white); outline-offset: 0.25rem; } main:focus { outline: none; } img { max-width: 100%; }

Naturalmente, questa versione è abbastanza semplice, ma il mio file globals.css di solito non ha bisogno di diventare troppo grande. Qui, stilo gli elementi HTML di base (intestazioni, corpo, collegamenti e così via). Non è necessario racchiudere questi elementi nei componenti di React o aggiungere costantemente classi solo per fornire uno stile di base.

Includo anche eventuali reimpostazioni degli stili di browser predefiniti . Di tanto in tanto, avrò uno stile di layout a livello di sito per fornire un "piè di pagina appiccicoso", ad esempio, ma appartengono qui solo se tutte le pagine condividono lo stesso layout. In caso contrario, sarà necessario definire l'ambito all'interno dei singoli componenti.

Includo sempre una sorta di stile :focus per indicare chiaramente gli elementi interattivi per gli utenti della tastiera quando sono concentrati. È meglio renderlo parte integrante del DNA del design del sito!

Ora, il nostro sito web sta iniziando a prendere forma:

Immagine del sito web in corso di lavorazione. Lo sfondo della pagina è ora di colore blu scuro e il titolo "Tè rilassanti" è verde. Il sito Web non ha layout/spaziatura e quindi si estende completamente alla larghezza della finestra del browser.
Immagine del sito web in corso di lavorazione. Lo sfondo della pagina è ora di colore blu scuro e il titolo "Tè rilassanti" è verde. Il sito Web non ha layout/spaziatura e quindi si estende completamente alla larghezza della finestra del browser. (Grande anteprima)

Classi di utilità

Un'area in cui la nostra homepage potrebbe sicuramente migliorare è che il testo attualmente si estende sempre ai lati dello schermo, quindi limitiamo la sua larghezza. Abbiamo bisogno di questo layout su questa pagina, ma immagino che potremmo averne bisogno anche su altre pagine. Questo è un ottimo caso d'uso per una classe di utilità!

Cerco di usare le classi di utilità con parsimonia piuttosto che come sostituto della semplice scrittura di CSS. I miei criteri personali per quando ha senso aggiungerne uno a un progetto sono:

  1. ne ho bisogno ripetutamente;
  2. Fa una cosa bene;
  3. Si applica a una gamma di componenti o pagine differenti.

Penso che questo caso soddisfi tutti e tre i criteri, quindi creiamo un nuovo file CSS styles/utilities.css e aggiungiamo:

 .lockup { max-width: 90ch; margin: 0 auto; }

Quindi aggiungiamo import '../styles/utilities.css' alle nostre pagine/_app.jsx . Infine, cambiamo il tag <main> nelle nostre pagine/index.jsx in <main className="lockup"> .

Ora, la nostra pagina si unisce ancora di più. Poiché abbiamo utilizzato la proprietà max-width , non abbiamo bisogno di query multimediali per rendere il nostro layout mobile responsive. E, poiché abbiamo utilizzato l'unità di misura ch , che equivale a circa la larghezza di un carattere, il nostro dimensionamento è dinamico rispetto alla dimensione del carattere del browser dell'utente.

Lo stesso sito Web di prima, ma ora il testo viene bloccato nel mezzo e non diventa troppo largo
Lo stesso sito Web di prima, ma ora il testo viene bloccato nel mezzo e non diventa troppo largo. (Grande anteprima)

Con la crescita del nostro sito Web, possiamo continuare ad aggiungere più classi di utilità. Qui adotto un approccio abbastanza utilitaristico: se lavoro e scopro che ho bisogno di un'altra classe per un colore o qualcosa del genere, la aggiungo. Non aggiungo tutte le classi possibili sotto il sole: aumenterebbe le dimensioni del file CSS e renderebbe il mio codice confuso. A volte, in progetti più grandi, mi piace suddividere le cose in una directory styles/utilities/ con alcuni file diversi; dipende dalle esigenze del progetto.

Possiamo pensare alle classi di utilità come al nostro toolkit di comandi di stile comuni e ripetuti condivisi a livello globale. Ci aiutano a impedirci di riscrivere costantemente lo stesso CSS tra diversi componenti.

Stili dei componenti

Per il momento abbiamo terminato la nostra homepage, ma dobbiamo ancora costruire un pezzo del nostro sito web: il negozio online. Il nostro obiettivo qui sarà quello di visualizzare una griglia di carte di tutti i tè che vogliamo vendere , quindi dovremo aggiungere alcuni componenti al nostro sito.

Iniziamo aggiungendo una nuova pagina su pages/shop.jsx :

 export default function Shop() { return <main> <div className="lockup"> <h1>Shop Our Teas</h1> </div> </main> }

Quindi, avremo bisogno di alcuni tè da mostrare. Includeremo un nome, una descrizione e un'immagine (nella directory public/) per ogni tè:

 const teas = [ { name: "Oolong", description: "A partially fermented tea.", image: "/oolong.jpg" }, // ... ]

Nota : questo non è un articolo sul recupero dei dati, quindi abbiamo preso la strada più semplice e abbiamo definito un array all'inizio del file.

Successivamente, dovremo definire un componente per visualizzare i nostri tè. Iniziamo creando una directory components/ (Next.js non lo rende per impostazione predefinita). Quindi, aggiungiamo una directory components/TeaList . Per qualsiasi componente che finisce per aver bisogno di più di un file, di solito metto tutti i file correlati all'interno di una cartella. In questo modo si evita che i nostri components/ cartella diventino non navigabili.

Ora aggiungiamo il nostro file Components/TeaList/TeaList.jsx :

 import TeaListItem from './TeaListItem' const TeaList = (props) => { const { teas } = props return <ul role="list"> {teas.map(tea => <TeaListItem tea={tea} key={tea.name} />)} </ul> } export default TeaList

Lo scopo di questo componente è di scorrere i nostri tè e mostrare una voce di elenco per ciascuno, quindi ora definiamo il nostro componente componenti/TeaList/TeaListItem.jsx :

 import Image from 'next/image' const TeaListItem = (props) => { const { tea } = props return <li> <div> <Image src={tea.image} alt="" objectFit="cover" objectPosition="center" layout="fill" /> </div> <div> <h2>{tea.name}</h2> <p>{tea.description}</p> </div> </li> } export default TeaListItem

Nota che stiamo usando il componente immagine integrato di Next.js. Ho impostato l'attributo alt su una stringa vuota perché in questo caso le immagini sono puramente decorative; vogliamo evitare di impantanare gli utenti di screen reader con lunghe descrizioni di immagini qui.

Infine, creiamo un file Components/TeaList/index.js , in modo che i nostri componenti siano facili da importare esternamente:

 import TeaList from './TeaList' import TeaListItem from './TeaListItem' export { TeaListItem } export default TeaList

E poi, colleghiamo tutto insieme aggiungendo import TeaList da ../components/TeaList e un <TeaList teas={teas} /> alla nostra pagina Shop. Ora, i nostri tè appariranno in un elenco, ma non sarà così bello.

Colocazione di stile con componenti tramite moduli CSS

Iniziamo con lo stile delle nostre carte (il componente TeaListLitem ). Ora, per la prima volta nel nostro progetto, vorremo aggiungere uno stile specifico per un solo componente. Creiamo un nuovo file components/TeaList/TeaListItem.module.css .

Potresti chiederti del modulo nell'estensione del file. Questo è un modulo CSS . Next.js supporta i moduli CSS e include una buona documentazione su di essi. Quando scriviamo il nome di una classe da un modulo CSS come .TeaListItem , verrà automaticamente trasformato in qualcosa di più simile a . TeaListItem_TeaListItem__TFOk_ . TeaListItem_TeaListItem__TFOk_ con un sacco di caratteri extra aggiunti. Di conseguenza, possiamo utilizzare qualsiasi nome di classe desideriamo senza preoccuparci che possa entrare in conflitto con altri nomi di classi in altre parti del nostro sito.

Un altro vantaggio dei moduli CSS sono le prestazioni. Next.js include una funzione di importazione dinamica. next/dynamic ci consente di caricare in modo pigro i componenti in modo che il loro codice venga caricato solo quando necessario, anziché aggiungerlo all'intera dimensione del pacchetto. Se importiamo gli stili locali necessari nei singoli componenti, gli utenti possono anche caricare in modo lento il CSS per i componenti importati dinamicamente . Per progetti di grandi dimensioni, possiamo scegliere di caricare in modo pigro porzioni significative del nostro codice e caricare in anticipo solo il JS/CSS più necessario. Di conseguenza, di solito finisco per creare un nuovo file CSS Module per ogni nuovo componente che necessita di uno stile locale.

Iniziamo aggiungendo alcuni stili iniziali al nostro file:

 .TeaListItem { display: flex; flex-direction: column; gap: var(--space-sm); background-color: var(--color, var(--off-white)); color: var(--dark); border-radius: 3px; box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); }

Quindi, possiamo importare lo stile da ./TeaListItem.module.css nel nostro componente TeaListitem . La variabile di stile arriva come un oggetto JavaScript, quindi possiamo accedere a questo style.TeaListItem.

Nota : il nome della nostra classe non ha bisogno di essere scritto in maiuscolo. Ho scoperto che una convenzione di nomi di classi in maiuscolo all'interno dei moduli (e quelli minuscoli all'esterno) differenzia visivamente i nomi delle classi locali e globali.

Quindi, prendiamo la nostra nuova classe locale e assegniamola a <li> nel nostro componente TeaListItem :

 <li className={style.TeaListComponent}>

Potresti chiederti della linea del colore di sfondo (cioè var(--color, var(--off-white)); ). Ciò che questo frammento significa è che per impostazione predefinita lo sfondo sarà il nostro valore --off-white . Ma, se impostiamo una proprietà personalizzata --color su una carta, questa sovrascriverà e sceglierà invece quel valore.

All'inizio, vorremo che tutte le nostre carte siano --off-white , ma potremmo voler cambiare il valore delle singole carte in un secondo momento. Funziona in modo molto simile agli oggetti di scena in React. Possiamo impostare un valore predefinito ma creare uno slot in cui possiamo scegliere altri valori in circostanze specifiche. Quindi, ci incoraggio a pensare a proprietà personalizzate CSS come la versione CSS di props .

Lo stile non sarà comunque eccezionale perché vogliamo assicurarci che le immagini rimangano all'interno dei loro contenitori. Il componente Immagine di Next.js con il prop layout="fill" ottiene position: absolute; dal framework, quindi possiamo limitare le dimensioni inserendo un contenitore con posizione: relativo;.

Aggiungiamo una nuova classe al nostro TeaListItem.module.css :

 .ImageContainer { position: relative; width: 100%; height: 10em; overflow: hidden; }

E poi aggiungiamo className={styles.ImageContainer} sul <div> che contiene il nostro <Image> . Uso nomi relativamente "semplici" come ImageContainer perché siamo all'interno di un modulo CSS, quindi non dobbiamo preoccuparci di entrare in conflitto con lo stile esterno.

Infine, vogliamo aggiungere un po' di padding ai lati del testo, quindi aggiungiamo un'ultima classe e facciamo affidamento sulle variabili di spaziatura che abbiamo impostato come token di progettazione:

 .Title { padding-left: var(--space-sm); padding-right: var(--space-sm); }

Possiamo aggiungere questa classe al <div> che contiene il nostro nome e la nostra descrizione. Ora, le nostre carte non sembrano così male:

Vengono visualizzate le carte per 3 diversi tè che sono stati aggiunti come dati sui semi. Hanno immagini, nomi e descrizioni. Attualmente vengono visualizzati in un elenco verticale senza spazio tra di loro.
Vengono visualizzate le carte per 3 diversi tè che sono stati aggiunti come dati sui semi. Hanno immagini, nomi e descrizioni. Attualmente vengono visualizzati in un elenco verticale senza spazio tra di loro. (Grande anteprima)

Combinazione di stile globale e locale

Successivamente, vogliamo che le nostre carte vengano visualizzate in un layout a griglia. In questo caso, siamo solo al confine tra gli stili locali e globali. Potremmo certamente codificare il nostro layout direttamente sul componente TeaList . Ma potrei anche immaginare che avere una classe di utilità che trasforma un elenco in un layout di griglia potrebbe essere utile in molti altri posti.

Prendiamo qui l'approccio globale e aggiungiamo una nuova classe di utilità nel nostro styles/utilities.css :

 .grid { list-style: none; display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--min-item-width, 30ch), 1fr)); gap: var(--space-md); }

Ora possiamo aggiungere la classe .grid a qualsiasi elenco e otterremo un layout della griglia automaticamente reattivo. Possiamo anche modificare la proprietà personalizzata --min-item-width (per impostazione predefinita 30ch ) per modificare la larghezza minima di ciascun elemento.

Nota : ricorda di pensare a proprietà personalizzate come oggetti di scena! Se questa sintassi non ti sembra familiare, puoi dare un'occhiata a "Griglia CSS intrinsecamente reattiva con minmax() e min() " di Chris Coyier.

Poiché abbiamo scritto questo stile a livello globale, non è richiesta alcuna fantasia per aggiungere className="grid" al nostro componente TeaList . Ma diciamo che vogliamo accoppiare questo stile globale con qualche negozio locale aggiuntivo. Ad esempio, vogliamo portare un po' più di "estetica del tè" e fare in modo che ogni altra carta abbia uno sfondo verde. Tutto quello che dobbiamo fare è creare un nuovo file Components/TeaList/TeaList.module.css :

 .TeaList > :nth-child(even) { --color: var(--green); }

Ricordi come abbiamo creato una proprietà --color custom sul nostro componente TeaListItem ? Bene, ora possiamo impostarlo in circostanze specifiche. Nota che possiamo ancora utilizzare i selettori figlio all'interno dei moduli CSS e non importa se stiamo selezionando un elemento con uno stile all'interno di un modulo diverso. Quindi, possiamo anche usare i nostri stili di componenti locali per influenzare i componenti figli. Questa è una caratteristica piuttosto che un bug, in quanto ci permette di sfruttare la cascata CSS ! Se provassimo a replicare questo effetto in un altro modo, probabilmente ci ritroveremmo con una sorta di zuppa JavaScript anziché tre righe di CSS.

Quindi, come possiamo mantenere la classe .grid globale sul nostro componente TeaList aggiungendo anche la classe .TeaList locale? È qui che la sintassi può diventare un po' eccentrica perché dobbiamo accedere alla nostra classe .TeaList dal modulo CSS facendo qualcosa come style.TeaList .

Un'opzione sarebbe quella di utilizzare l'interpolazione di stringhe per ottenere qualcosa del tipo:

 <ul role="list" className={`${style.TeaList} grid`}>

In questo piccolo caso, questo potrebbe essere abbastanza buono. Se stiamo combinando più classi, trovo che questa sintassi faccia esplodere un po' il mio cervello, quindi a volte sceglierò di usare la libreria dei nomi delle classi. In questo caso, ci ritroviamo con un elenco dall'aspetto più sensato:

 <ul role="list" className={classnames(style.TeaList, "grid")}>

Ora abbiamo completato la nostra pagina Shop e abbiamo fatto in modo che il nostro componente TeaList sfrutti gli stili globali e locali.

Le nostre carte da tè ora vengono visualizzate in una griglia. Gli interi pari sono colorati in verde, mentre le voci dispari sono bianche.
Le nostre carte da tè ora vengono visualizzate in una griglia. Gli interi pari sono colorati in verde, mentre le voci dispari sono bianche. (Grande anteprima)

Un atto di bilanciamento

Ora abbiamo costruito il nostro negozio di tè utilizzando solo semplici CSS per gestire lo styling. Potresti aver notato che non abbiamo dovuto passare anni a occuparci di configurazioni Webpack personalizzate, installazione di librerie esterne e così via. Ciò è dovuto ai modelli che abbiamo utilizzato per lavorare con Next.js fuori dagli schemi. Inoltre, incoraggiano le migliori pratiche CSS e si adattano naturalmente all'architettura del framework Next.js.

La nostra organizzazione CSS era composta da quattro elementi chiave:

  1. Gettoni di design,
  2. Stili globali,
  3. Classi di utilità,
  4. Stili dei componenti.

Man mano che continuiamo a costruire il nostro sito, il nostro elenco di token di progettazione e classi di utilità aumenterà. Qualsiasi stile che non ha senso da aggiungere come classe di utilità, possiamo aggiungerlo agli stili dei componenti usando i moduli CSS. Di conseguenza, possiamo trovare un equilibrio continuo tra le preoccupazioni di stile locali e globali. Possiamo anche generare codice CSS intuitivo e performante che cresce naturalmente insieme al nostro sito Next.js.