Style global ou local dans Next.js
Publié: 2022-03-10J'ai eu une grande expérience en utilisant Next.js pour gérer des projets front-end complexes. Next.js a une opinion sur la façon d'organiser le code JavaScript, mais il n'a pas d'opinions intégrées sur la façon d'organiser le CSS.
Après avoir travaillé dans le cadre, j'ai trouvé une série de modèles organisationnels qui, selon moi, sont à la fois conformes aux philosophies directrices de Next.js et appliquent les meilleures pratiques CSS. Dans cet article, nous allons créer ensemble un site Web (un magasin de thé !) pour illustrer ces modèles.
Remarque : Vous n'aurez probablement pas besoin d'une expérience préalable de Next.js, bien qu'il soit bon d'avoir une compréhension de base de React et d'être ouvert à l'apprentissage de nouvelles techniques CSS.
Rédaction de CSS "à l'ancienne"
Lorsque nous examinons pour la première fois Next.js, nous pouvons être tentés d'envisager d'utiliser une sorte de bibliothèque CSS-in-JS. Bien qu'il puisse y avoir des avantages en fonction du projet, CSS-in-JS introduit de nombreuses considérations techniques. Cela nécessite l'utilisation d'une nouvelle bibliothèque externe, ce qui augmente la taille du bundle. CSS-in-JS peut également avoir un impact sur les performances en provoquant des rendus et des dépendances supplémentaires sur l'état global.
Lecture recommandée : " Les coûts de performance invisibles des bibliothèques CSS-in-JS modernes dans les applications React)" par Aggelos Arvanitakis
De plus, l'intérêt d'utiliser une bibliothèque comme Next.js est de rendre les ressources de manière statique chaque fois que possible, il n'est donc pas si logique d'écrire du JS qui doit être exécuté dans le navigateur pour générer du CSS.
Il y a quelques questions que nous devons prendre en compte lors de l'organisation du style dans Next.js :
Comment pouvons-nous respecter les conventions/meilleures pratiques du cadre ?
Comment pouvons-nous équilibrer les préoccupations de style « globales » (polices, couleurs, mises en page principales, etc.) avec celles « locales » (styles concernant les composants individuels) ?
La réponse que j'ai trouvée à la première question est d' écrire simplement du bon vieux CSS . Non seulement Next.js prend en charge cette opération sans configuration supplémentaire ; il donne également des résultats performants et statiques.
Pour résoudre le deuxième problème, j'adopte une approche qui peut se résumer en quatre parties :
- Concevoir des jetons
- Styles globaux
- Classes utilitaires
- Styles de composants
Je suis redevable à l'idée d'Andy Bell de CUBE CSS ("Composition, Utilité, Bloc, Exception") ici. Si vous n'avez jamais entendu parler de ce principe d'organisation auparavant, je vous recommande de consulter son site officiel ou sa fonctionnalité sur le Smashing Podcast. L'un des principes que nous retiendrons de CUBE CSS est l'idée que nous devrions adopter plutôt que craindre la cascade CSS. Apprenons ces techniques en les appliquant à un projet de site Web.
Commencer
Nous allons construire un magasin de thé parce que, eh bien, le thé est bon. Nous allons commencer par exécuter yarn create next-app
pour créer un nouveau projet Next.js. Ensuite, nous supprimerons tout dans le styles/ directory
(ce ne sont que des exemples de code).
Remarque : Si vous souhaitez suivre le projet terminé, vous pouvez le consulter ici.
Jetons de conception
Dans à peu près n'importe quelle configuration CSS, il y a un avantage évident à stocker toutes les valeurs partagées globalement dans des variables . Si un client demande qu'une couleur change, la mise en œuvre du changement est une simple ligne plutôt qu'un gâchis massif de recherche et de remplacement. Par conséquent, un élément clé de notre configuration CSS Next.js stockera toutes les valeurs à l'échelle du site sous forme de jetons de conception .
Nous utiliserons les propriétés personnalisées CSS intégrées pour stocker ces jetons. (Si vous n'êtes pas familier avec cette syntaxe, vous pouvez consulter "Un guide stratégique pour les propriétés personnalisées CSS".) Je dois mentionner que (dans certains projets) j'ai choisi d'utiliser des variables SASS/SCSS à cette fin. Je n'ai trouvé aucun avantage réel, donc je n'inclus généralement SASS dans un projet que si j'ai besoin d'autres fonctionnalités SASS (mix-ins, itération, importation de fichiers, etc.). Les propriétés personnalisées CSS, en revanche, fonctionnent également avec la cascade et peuvent être modifiées au fil du temps plutôt que d'être compilées de manière statique. Donc, pour aujourd'hui, restons-en au CSS simple .
Dans notre répertoire styles/
, créons un nouveau fichier 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; }
Bien sûr, cette liste peut et va s'allonger avec le temps. Une fois que nous avons ajouté ce fichier, nous devons passer à notre fichier pages/_app.jsx , qui est la mise en page principale de toutes nos pages, et ajouter :
import '../styles/design_tokens.css'
J'aime penser que les jetons de conception sont le ciment qui maintient la cohérence tout au long du projet. Nous référencerons ces variables à l'échelle mondiale, ainsi que dans des composants individuels, garantissant un langage de conception unifié.
Styles globaux
Ensuite, ajoutons une page à notre site Web ! Sautons dans le fichier pages/index.jsx (c'est notre page d'accueil). Nous allons supprimer tout le passe-partout et ajouter quelque chose comme :
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> }
Malheureusement, cela semblera assez simple, alors définissons quelques styles globaux pour les éléments de base , par exemple les balises <h1>
. (J'aime penser à ces styles comme des "valeurs globales par défaut raisonnables".) Nous pouvons les remplacer dans des cas spécifiques, mais ils sont une bonne estimation de ce que nous voudrons si nous ne le faisons pas.
Je vais mettre ceci dans le fichier styles/globals.css (qui vient par défaut de 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%; }
Bien sûr, cette version est assez basique, mais mon fichier globals.css n'a généralement pas besoin d'être trop volumineux. Ici, je stylise les éléments HTML de base (titres, corps, liens, etc.). Il n'est pas nécessaire d'envelopper ces éléments dans des composants React ou d'ajouter constamment des classes juste pour fournir un style de base.
J'inclus également toutes les réinitialisations des styles de navigateur par défaut . De temps en temps, j'aurai un style de mise en page à l'échelle du site pour fournir un "pied de page collant", par exemple, mais ils n'appartiennent ici que si toutes les pages partagent la même mise en page. Sinon, il devra être délimité à l'intérieur des composants individuels.
J'inclus toujours une sorte de style :focus
pour indiquer clairement les éléments interactifs pour les utilisateurs de clavier lorsqu'ils sont ciblés. Mieux vaut en faire une partie intégrante de l'ADN design du site !
Maintenant, notre site Web commence à prendre forme :
Classes utilitaires
Un domaine où notre page d'accueil pourrait certainement s'améliorer est que le texte s'étend actuellement toujours sur les côtés de l'écran, limitons donc sa largeur. Nous avons besoin de cette mise en page sur cette page, mais j'imagine que nous pourrions en avoir besoin sur d'autres pages également. C'est un excellent cas d'utilisation pour une classe utilitaire !
J'essaie d' utiliser les classes utilitaires avec parcimonie plutôt que de remplacer simplement l'écriture de CSS. Mes critères personnels pour savoir quand il est logique d'en ajouter un à un projet sont :
- j'en ai besoin à plusieurs reprises;
- Il fait bien une chose;
- Il s'applique à une gamme de composants ou de pages différents.
Je pense que ce cas répond aux trois critères, alors créons un nouveau fichier CSS styles/utilities.css et ajoutons :
.lockup { max-width: 90ch; margin: 0 auto; }
Ajoutons ensuite import '../styles/utilities.css'
à nos pages/_app.jsx . Enfin, changeons la <main>
dans nos pages/index.jsx en <main className="lockup">
.
Maintenant, notre page se rassemble encore plus. Parce que nous avons utilisé la propriété max-width
, nous n'avons pas besoin de requêtes multimédias pour rendre notre mise en page mobile réactive. Et, parce que nous avons utilisé l'unité de mesure ch
- qui équivaut à environ la largeur d'un caractère - notre dimensionnement est dynamique en fonction de la taille de police du navigateur de l'utilisateur.
Au fur et à mesure que notre site Web se développe, nous pouvons continuer à ajouter plus de classes utilitaires. J'adopte ici une approche assez utilitaire : si je travaille et trouve que j'ai besoin d'une autre classe pour une couleur ou quelque chose, je l'ajoute. Je n'ajoute pas toutes les classes possibles sous le soleil - cela augmenterait la taille du fichier CSS et rendrait mon code confus. Parfois, dans des projets plus importants, j'aime diviser les choses en un répertoire styles/utilities/
avec quelques fichiers différents ; cela dépend des besoins du projet.
Nous pouvons considérer les classes utilitaires comme notre boîte à outils de commandes de style communes et répétées qui sont partagées à l'échelle mondiale. Ils nous aident à éviter de réécrire constamment le même CSS entre différents composants.
Styles de composant
Nous avons terminé notre page d'accueil pour le moment, mais nous devons encore construire une partie de notre site Web : la boutique en ligne. Notre objectif ici sera d'afficher une grille de fiches de tous les thés que nous souhaitons vendre , il nous faudra donc ajouter quelques composants à notre site.
Commençons par ajouter une nouvelle page à pages/shop.jsx :
export default function Shop() { return <main> <div className="lockup"> <h1>Shop Our Teas</h1> </div> </main> }
Ensuite, nous aurons besoin de quelques thés à afficher. Nous inclurons un nom, une description et une image (dans le répertoire public/) pour chaque thé :
const teas = [ { name: "Oolong", description: "A partially fermented tea.", image: "/oolong.jpg" }, // ... ]
Note : Ceci n'est pas un article sur la récupération de données, nous avons donc choisi la voie la plus simple et défini un tableau au début du fichier.
Ensuite, nous devrons définir un composant pour afficher nos thés. Commençons par créer un répertoire components/
(Next.js ne le fait pas par défaut). Ensuite, ajoutons un répertoire components/TeaList
. Pour tout composant qui nécessite plus d'un fichier, je place généralement tous les fichiers associés dans un dossier. Cela empêche notre dossier components/
de devenir non navigable.
Ajoutons maintenant notre fichier 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
Le but de ce composant est d'itérer sur nos thés et d'afficher un élément de liste pour chacun, alors définissons maintenant notre composant components/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
Notez que nous utilisons le composant image intégré de Next.js. J'ai défini l'attribut alt
sur une chaîne vide car les images sont purement décoratives dans ce cas ; nous voulons éviter d'enliser les utilisateurs de lecteurs d'écran avec de longues descriptions d'images ici.
Enfin, créons un fichier components/TeaList/index.js , afin que nos composants soient faciles à importer en externe :
import TeaList from './TeaList' import TeaListItem from './TeaListItem' export { TeaListItem } export default TeaList
Et puis, connectons le tout en ajoutant import TeaList from ../components/TeaList
et un <TeaList teas={teas} />
à notre page Shop. Maintenant, nos thés apparaîtront dans une liste, mais ce ne sera pas si joli.
Colocaliser le style avec des composants via des modules CSS
Commençons par styliser nos cartes (le composant TeaListLitem
). Maintenant, pour la première fois dans notre projet, nous allons vouloir ajouter un style spécifique à un seul composant. Créons un nouveau fichier components/TeaList/TeaListItem.module.css .
Vous vous interrogez peut-être sur le module dans l'extension de fichier. Il s'agit d'un module CSS . Next.js prend en charge les modules CSS et inclut une bonne documentation à leur sujet. Lorsque nous écrivons un nom de classe à partir d'un module CSS tel que .TeaListItem
, il sera automatiquement transformé en quelque chose qui ressemble plus à . TeaListItem_TeaListItem__TFOk_
. TeaListItem_TeaListItem__TFOk_
avec un tas de caractères supplémentaires ajoutés. Par conséquent, nous pouvons utiliser n'importe quel nom de classe que nous voulons sans craindre qu'il n'entre en conflit avec d'autres noms de classe ailleurs sur notre site.
Un autre avantage des modules CSS est la performance. Next.js inclut une fonctionnalité d'importation dynamique. next/dynamic nous permet de charger les composants paresseux afin que leur code ne soit chargé qu'en cas de besoin, plutôt que d'ajouter à la taille totale du bundle. Si nous importons les styles locaux nécessaires dans des composants individuels, les utilisateurs peuvent également charger paresseusement le CSS pour les composants importés dynamiquement . Pour les grands projets, nous pouvons choisir de charger paresseusement des morceaux importants de notre code et de ne charger que le JS/CSS le plus nécessaire à l'avance. En conséquence, je finis généralement par créer un nouveau fichier de module CSS pour chaque nouveau composant nécessitant un style local.
Commençons par ajouter quelques styles initiaux à notre fichier :
.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); }
Ensuite, nous pouvons importer le style de ./TeaListItem.module.css
dans notre composant TeaListitem
. La variable de style se présente comme un objet JavaScript, nous pouvons donc accéder à ce style.TeaListItem.
Remarque : Notre nom de classe n'a pas besoin d'être en majuscule. J'ai découvert qu'une convention de noms de classe en majuscules à l'intérieur des modules (et en minuscules à l'extérieur) différencie visuellement les noms de classe locaux et globaux.
Prenons donc notre nouvelle classe locale et affectons-la au <li>
dans notre composant TeaListItem
:
<li className={style.TeaListComponent}>
Vous vous posez peut-être des questions sur la ligne de couleur d'arrière-plan (c'est-à-dire var(--color, var(--off-white));
). Ce que cet extrait signifie, c'est que par défaut , l'arrière-plan sera notre valeur --off-white
. Mais, si nous définissons une propriété personnalisée --color
sur une carte, elle remplacera et choisira cette valeur à la place.
Au début, nous voudrons que toutes nos cartes soient --off-white
, mais nous voudrons peut-être changer la valeur des cartes individuelles plus tard. Cela fonctionne de manière très similaire aux accessoires dans React. Nous pouvons définir une valeur par défaut mais créer un emplacement où nous pouvons choisir d'autres valeurs dans des circonstances spécifiques. Donc, je nous encourage à penser aux propriétés personnalisées CSS comme la version CSS de props .
Le style n'aura toujours pas fière allure car nous voulons nous assurer que les images restent dans leurs conteneurs. Le composant Image de Next.js avec le prop layout="fill"
obtient position: absolute;
à partir du cadre, nous pouvons donc limiter la taille en mettant dans un conteneur avec la position : relative ;.
Ajoutons une nouvelle classe à notre TeaListItem.module.css :
.ImageContainer { position: relative; width: 100%; height: 10em; overflow: hidden; }
Et puis ajoutons className={styles.ImageContainer}
sur le <div>
qui contient notre <Image>
. J'utilise des noms relativement "simples" tels que ImageContainer
car nous sommes à l'intérieur d'un module CSS, nous n'avons donc pas à nous soucier des conflits avec le style extérieur.
Enfin, nous voulons ajouter un peu de rembourrage sur les côtés du texte, alors ajoutons une dernière classe et comptons sur les variables d'espacement que nous avons configurées en tant que jetons de conception :
.Title { padding-left: var(--space-sm); padding-right: var(--space-sm); }
Nous pouvons ajouter cette classe au <div>
qui contient notre nom et notre description. Maintenant, nos cartes n'ont pas l'air si mal :
Combiner le style global et local
Ensuite, nous voulons que nos cartes s'affichent sous forme de grille. Dans ce cas, nous sommes juste à la frontière entre les styles locaux et globaux. Nous pourrions certainement coder notre mise en page directement sur le composant TeaList
. Mais, je pourrais aussi imaginer qu'avoir une classe utilitaire qui transforme une liste en une disposition de grille pourrait être utile à plusieurs autres endroits.
Prenons l'approche globale ici et ajoutons une nouvelle classe utilitaire dans notre 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); }
Maintenant, nous pouvons ajouter la classe .grid
sur n'importe quelle liste, et nous obtiendrons une mise en page de grille automatiquement réactive. Nous pouvons également modifier la propriété personnalisée --min-item-width
(par défaut 30ch
) pour modifier la largeur minimale de chaque élément.
Remarque : N'oubliez pas de considérer les propriétés personnalisées comme des accessoires ! Si cette syntaxe ne vous semble pas familière, vous pouvez consulter « Grille CSS intrinsèquement réactive avec minmax()
et min()
» par Chris Coyier.
Comme nous avons écrit ce style globalement, il n'est pas nécessaire d'ajouter className="grid"
à notre composant TeaList
. Mais, disons que nous voulons coupler ce style global avec un magasin local supplémentaire. Par exemple, nous voulons apporter un peu plus de "l'esthétique du thé" et faire en sorte que toutes les autres cartes aient un fond vert. Tout ce que nous avons à faire est de créer un nouveau fichier components/TeaList/TeaList.module.css :
.TeaList > :nth-child(even) { --color: var(--green); }
Rappelez-vous comment nous avons créé une propriété --color custom
sur notre composant TeaListItem
? Eh bien, maintenant nous pouvons le définir dans des circonstances spécifiques. Notez que nous pouvons toujours utiliser des sélecteurs enfants dans les modules CSS, et peu importe que nous sélectionnions un élément stylé dans un module différent. Ainsi, nous pouvons également utiliser nos styles de composants locaux pour affecter les composants enfants. C'est une fonctionnalité plutôt qu'un bug, car cela nous permet de profiter de la cascade CSS ! Si nous essayions de reproduire cet effet d'une autre manière, nous nous retrouverions probablement avec une sorte de soupe JavaScript plutôt que trois lignes de CSS.
Alors, comment pouvons-nous conserver la classe globale .grid
sur notre composant TeaList
tout en ajoutant la classe locale .TeaList
? C'est là que la syntaxe peut devenir un peu funky car nous devons accéder à notre classe .TeaList
hors du module CSS en faisant quelque chose comme style.TeaList
.
Une option serait d'utiliser l'interpolation de chaîne pour obtenir quelque chose comme :
<ul role="list" className={`${style.TeaList} grid`}>
Dans ce petit boîtier, cela pourrait suffire. Si nous mélangeons et associons plus de classes, je trouve que cette syntaxe fait un peu exploser mon cerveau, donc je choisirai parfois d'utiliser la bibliothèque des noms de classe. Dans ce cas, nous nous retrouvons avec une liste plus sensée :
<ul role="list" className={classnames(style.TeaList, "grid")}>
Maintenant, nous avons terminé notre page Boutique et nous avons fait en sorte que notre composant TeaList
tire parti des styles globaux et locaux.
Un exercice d'équilibre
Nous avons maintenant construit notre boutique de thé en utilisant uniquement du CSS simple pour gérer le style. Vous avez peut-être remarqué que nous n'avons pas eu à passer des heures à nous occuper des configurations Webpack personnalisées, à installer des bibliothèques externes, etc. C'est parce que les modèles que nous avons utilisés fonctionnent avec Next.js prêts à l'emploi. De plus, ils encouragent les meilleures pratiques CSS et s'intègrent naturellement dans l'architecture du framework Next.js.
Notre organisation CSS se composait de quatre éléments clés :
- Concevoir des jetons,
- Styles globaux,
- Classes utilitaires,
- Styles de composants.
Au fur et à mesure que nous continuons à construire notre site, notre liste de jetons de conception et de classes utilitaires s'allongera. Tout style qui n'a pas de sens à ajouter en tant que classe utilitaire, nous pouvons l'ajouter aux styles de composants à l'aide de modules CSS. En conséquence, nous pouvons trouver un équilibre continu entre les préoccupations de style locales et globales. Nous pouvons également générer un code CSS performant et intuitif qui se développe naturellement parallèlement à notre site Next.js.