Estilo global frente a local en Next.js

Publicado: 2022-03-10
Resumen rápido ↬ Next.js tiene fuertes opiniones sobre cómo organizar JavaScript pero no CSS. ¿Cómo podemos desarrollar patrones que fomenten las mejores prácticas de CSS y al mismo tiempo seguir la lógica del marco? La respuesta es sorprendentemente simple: escribir CSS bien estructurado que equilibre las preocupaciones de estilo globales y locales.

He tenido una gran experiencia usando Next.js para administrar proyectos front-end complejos. Next.js tiene opiniones sobre cómo organizar el código JavaScript, pero no tiene opiniones integradas sobre cómo organizar CSS.

Después de trabajar dentro del marco, encontré una serie de patrones organizativos que creo que se ajustan a las filosofías rectoras de Next.js y ejercen las mejores prácticas de CSS. En este artículo, construiremos juntos un sitio web (¡una tienda de té!) para demostrar estos patrones.

Nota : probablemente no necesitará experiencia previa en Next.js, aunque sería bueno tener una comprensión básica de React y estar abierto a aprender algunas técnicas nuevas de CSS.

Escribir CSS "anticuado"

Cuando examinamos Next.js por primera vez, podemos tener la tentación de considerar el uso de algún tipo de biblioteca CSS-in-JS. Aunque puede haber beneficios según el proyecto, CSS-in-JS introduce muchas consideraciones técnicas. Requiere el uso de una nueva biblioteca externa, que se suma al tamaño del paquete. CSS-in-JS también puede tener un impacto en el rendimiento al generar renderizaciones y dependencias adicionales en el estado global.

Lectura recomendada : " Los costos de rendimiento ocultos de las bibliotecas CSS-in-JS modernas en aplicaciones React)" por Aggelos Arvanitakis

Además, el objetivo de usar una biblioteca como Next.js es representar activos de forma estática siempre que sea posible, por lo que no tiene mucho sentido escribir JS que deba ejecutarse en el navegador para generar CSS.

Hay un par de preguntas que debemos considerar al organizar el estilo dentro de Next.js:

¿Cómo podemos encajar dentro de las convenciones/mejores prácticas del marco?

¿Cómo podemos equilibrar las preocupaciones de estilo "globales" (fuentes, colores, diseños principales, etc.) con las "locales" (estilos relacionados con componentes individuales)?

La respuesta que se me ocurrió para la primera pregunta es simplemente escribir un buen CSS a la antigua . Next.js no solo admite hacerlo sin configuración adicional; también produce resultados que son eficientes y estáticos.

Para resolver el segundo problema, tomo un enfoque que se puede resumir en cuatro partes:

  1. Fichas de diseño
  2. Estilos globales
  3. Clases de utilidad
  4. Estilos de componentes

Estoy en deuda con la idea de Andy Bell de CUBE CSS ("Composición, Utilidad, Bloque, Excepción") aquí. Si no ha oído hablar de este principio organizativo antes, le recomendé consultar su sitio oficial o su función en Smashing Podcast. Uno de los principios que tomaremos de CUBE CSS es la idea de que debemos abrazar en lugar de temer la cascada de CSS. Aprendamos estas técnicas aplicándolas a un proyecto de sitio web.

Empezando

Construiremos una tienda de té porque, bueno, el té es sabroso. Comenzaremos ejecutando yarn create next-app para crear un nuevo proyecto Next.js. Luego, eliminaremos todo en el styles/ directory (todo es código de muestra).

Nota : si desea continuar con el proyecto terminado, puede consultarlo aquí.

fichas de diseño

En prácticamente cualquier configuración de CSS, existe un claro beneficio de almacenar todos los valores compartidos globalmente en variables . Si un cliente pide que se cambie un color, implementar el cambio es una sola línea en lugar de un lío masivo de buscar y reemplazar. En consecuencia, una parte clave de nuestra configuración de CSS de Next.js será almacenar todos los valores de todo el sitio como tokens de diseño .

Usaremos las propiedades personalizadas de CSS incorporadas para almacenar estos tokens. (Si no está familiarizado con esta sintaxis, puede consultar "Una guía de estrategia para las propiedades personalizadas de CSS".) Debo mencionar que (en algunos proyectos) he optado por usar variables SASS/SCSS para este propósito. No he encontrado ninguna ventaja real, por lo que generalmente solo incluyo SASS en un proyecto si descubro que necesito otras características de SASS (combinaciones, iteración, importación de archivos, etc.). Las propiedades personalizadas de CSS, por el contrario, también funcionan con la cascada y se pueden cambiar con el tiempo en lugar de compilar estáticamente. Entonces, por hoy, sigamos con CSS simple .

En nuestro directorio styles/ , hagamos un nuevo archivo 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; }

Por supuesto, esta lista puede y crecerá con el tiempo. Una vez que agreguemos este archivo, debemos ir a nuestro archivo pages/_app.jsx , que es el diseño principal de todas nuestras páginas, y agregar:

 import '../styles/design_tokens.css'

Me gusta pensar en los tokens de diseño como el pegamento que mantiene la coherencia en todo el proyecto. Haremos referencia a estas variables a escala global, así como dentro de los componentes individuales, asegurando un lenguaje de diseño unificado.

¡Más después del salto! Continúe leyendo a continuación ↓

Estilos globales

A continuación, agreguemos una página a nuestro sitio web. Vayamos al archivo pages/index.jsx (esta es nuestra página de inicio). Eliminaremos todos los repetitivos y agregaremos algo como:

 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> }

Desafortunadamente, parecerá bastante simple, así que configuremos algunos estilos globales para elementos básicos , por ejemplo, etiquetas <h1> . (Me gusta pensar en estos estilos como "valores predeterminados globales razonables".) Podemos anularlos en casos específicos, pero son una buena suposición de lo que querremos si no lo hacemos.

Pondré esto en el archivo styles/globals.css (que viene por defecto 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%; }

Por supuesto, esta versión es bastante básica, pero mi archivo globals.css generalmente no termina necesitando ser demasiado grande. Aquí, diseño elementos HTML básicos (encabezados, cuerpo, enlaces, etc.). No es necesario envolver estos elementos en componentes React o agregar clases constantemente solo para proporcionar un estilo básico.

También incluyo cualquier restablecimiento de los estilos de navegador predeterminados . De vez en cuando, tendré algún estilo de diseño en todo el sitio para proporcionar un "pie de página fijo", por ejemplo, pero solo pertenecen aquí si todas las páginas comparten el mismo diseño. De lo contrario, deberá incluirse en el ámbito dentro de los componentes individuales.

Siempre incluyo algún tipo de estilo :focus para indicar claramente los elementos interactivos para los usuarios del teclado cuando están enfocados. ¡Es mejor convertirlo en una parte integral del ADN del diseño del sitio!

Ahora, nuestro sitio web está comenzando a tomar forma:

Imagen del sitio web del trabajo en progreso. El fondo de la página ahora es de color azul oscuro y el título "Tés relajantes" es verde. El sitio web no tiene diseño/espaciado y, por lo tanto, se extiende completamente al ancho de la ventana del navegador.
Imagen del sitio web del trabajo en progreso. El fondo de la página ahora es de color azul oscuro y el título "Tés relajantes" es verde. El sitio web no tiene diseño/espaciado y, por lo tanto, se extiende completamente al ancho de la ventana del navegador. (Vista previa grande)

Clases de utilidad

Un área en la que nuestra página de inicio ciertamente podría mejorar es que el texto actualmente siempre se extiende a los lados de la pantalla, así que limitemos su ancho. Necesitamos este diseño en esta página, pero me imagino que también podríamos necesitarlo en otras páginas. ¡Este es un gran caso de uso para una clase de utilidad!

Trato de usar las clases de utilidad con moderación en lugar de reemplazar solo escribir CSS. Mis criterios personales sobre cuándo tiene sentido agregar uno a un proyecto son:

  1. Lo necesito repetidamente;
  2. Hace una cosa bien;
  3. Se aplica a través de una gama de diferentes componentes o páginas.

Creo que este caso cumple con los tres criterios, así que hagamos un nuevo archivo CSS styles/utilities.css y agreguemos:

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

Luego agreguemos import '../styles/utilities.css' a nuestras páginas/_app.jsx . Finalmente, cambiemos la etiqueta <main> en nuestras páginas/index.jsx a <main className="lockup"> .

Ahora, nuestra página se está uniendo aún más. Debido a que usamos la propiedad max-width , no necesitamos ninguna consulta de medios para que nuestro diseño responda a dispositivos móviles. Y, debido a que usamos la unidad de medida ch , que equivale aproximadamente al ancho de un carácter, nuestro tamaño es dinámico para el tamaño de fuente del navegador del usuario.

El mismo sitio web que antes, pero ahora el texto se sujeta en el medio y no se ensancha demasiado.
El mismo sitio web que antes, pero ahora el texto se sujeta en el medio y no se ensancha demasiado. (Vista previa grande)

A medida que crece nuestro sitio web, podemos continuar agregando más clases de utilidad. Tomo un enfoque bastante utilitario aquí: si estoy trabajando y descubro que necesito otra clase para un color o algo así, la agrego. No agrego todas las clases posibles bajo el sol: aumentaría el tamaño del archivo CSS y haría que mi código fuera confuso. A veces, en proyectos más grandes, me gusta dividir las cosas en un directorio de styles/utilities/ con algunos archivos diferentes; depende de las necesidades del proyecto.

Podemos pensar en las clases de utilidad como nuestro conjunto de herramientas de comandos de estilo comunes y repetidos que se comparten globalmente. Ayudan a evitar que reescribamos constantemente el mismo CSS entre diferentes componentes.

Estilos de componentes

Hemos terminado nuestra página de inicio por el momento, pero todavía tenemos que construir una parte de nuestro sitio web: la tienda en línea. Nuestro objetivo aquí será mostrar una cuadrícula de tarjeta de todos los tés que queremos vender , por lo que necesitaremos agregar algunos componentes a nuestro sitio.

Comencemos agregando una nueva página en pages/shop.jsx :

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

Entonces, necesitaremos algunos tés para exhibir. Incluiremos un nombre, una descripción y una imagen (en el directorio public/) para cada té:

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

Nota : este no es un artículo sobre la obtención de datos, por lo que tomamos la ruta fácil y definimos una matriz al comienzo del archivo.

A continuación, necesitaremos definir un componente para mostrar nuestros tés. Comencemos creando un directorio components/ (Next.js no hace esto por defecto). Luego, agreguemos un directorio de components/TeaList . Para cualquier componente que termine necesitando más de un archivo, generalmente coloco todos los archivos relacionados dentro de una carpeta. Si lo hace, evitará que nuestra carpeta components/ se vuelva innavegable.

Ahora, agreguemos nuestro archivo 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

El propósito de este componente es iterar sobre nuestros tés y mostrar un elemento de lista para cada uno, así que ahora definamos nuestro componente 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

Tenga en cuenta que estamos usando el componente de imagen integrado de Next.js. Establecí el atributo alt en una cadena vacía porque las imágenes son puramente decorativas en este caso; queremos evitar atascar a los usuarios de lectores de pantalla con descripciones de imágenes largas aquí.

Finalmente, hagamos un archivo components/TeaList/index.js , para que nuestros componentes sean fáciles de importar externamente:

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

Y luego, conectemos todo agregando import TeaList from ../components/TeaList y un <TeaList teas={teas} /> a nuestra página de la tienda. Ahora, nuestros tés aparecerán en una lista, pero no será tan bonito.

Estilo de colocación con componentes a través de módulos CSS

Empecemos diseñando nuestras tarjetas (el componente TeaListLitem ). Ahora, por primera vez en nuestro proyecto, vamos a querer agregar un estilo que sea específico para un solo componente. Vamos a crear un nuevo archivo components/TeaList/TeaListItem.module.css .

Quizás se esté preguntando sobre el módulo en la extensión de archivo. Este es un módulo CSS . Next.js admite módulos CSS e incluye buena documentación sobre ellos. Cuando escribimos un nombre de clase desde un módulo CSS como .TeaListItem , se transformará automáticamente en algo más parecido a . TeaListItem_TeaListItem__TFOk_ . TeaListItem_TeaListItem__TFOk_ con un montón de caracteres adicionales añadidos. En consecuencia, podemos usar cualquier nombre de clase que queramos sin preocuparnos de que entre en conflicto con otros nombres de clase en otras partes de nuestro sitio.

Otra ventaja de los módulos CSS es el rendimiento. Next.js incluye una función de importación dinámica. next/dynamic nos permite cargar componentes de forma diferida para que su código solo se cargue cuando sea necesario, en lugar de aumentar el tamaño del paquete completo. Si importamos los estilos locales necesarios en componentes individuales, los usuarios también pueden cargar de forma diferida el CSS para componentes importados dinámicamente . Para proyectos grandes, podemos optar por cargar de forma diferida partes significativas de nuestro código y solo cargar el JS/CSS más necesario por adelantado. Como resultado, generalmente termino creando un nuevo archivo de módulo CSS para cada componente nuevo que necesita un estilo local.

Comencemos agregando algunos estilos iniciales a nuestro archivo:

 .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); }

Luego, podemos importar el estilo desde ./TeaListItem.module.css en nuestro componente TeaListitem . La variable de estilo viene como un objeto de JavaScript, por lo que podemos acceder a este estilo similar a una style.TeaListItem.

Nota : el nombre de nuestra clase no necesita escribirse en mayúsculas. Descubrí que una convención de nombres de clase en mayúsculas dentro de los módulos (y en minúsculas fuera) diferencia visualmente los nombres de clase locales frente a los globales.

Entonces, tomemos nuestra nueva clase local y asignémosla a <li> en nuestro componente TeaListItem :

 <li className={style.TeaListComponent}>

Quizás se esté preguntando acerca de la línea de color de fondo (es decir var(--color, var(--off-white)); ). Lo que significa este fragmento es que , de forma predeterminada , el fondo será nuestro valor --off-white . Pero, si establecemos una propiedad personalizada --color en una tarjeta, se anulará y elegirá ese valor en su lugar.

Al principio, querremos que todas nuestras tarjetas sean --off-white , pero es posible que deseemos cambiar el valor de las tarjetas individuales más adelante. Esto funciona de manera muy similar a los accesorios en React. Podemos establecer un valor predeterminado pero crear un espacio donde podemos elegir otros valores en circunstancias específicas. Por lo tanto, nos animo a pensar en las propiedades personalizadas de CSS como la versión de props de CSS .

El estilo aún no se verá muy bien porque queremos asegurarnos de que las imágenes permanezcan dentro de sus contenedores. El componente de imagen de Next.js con el accesorio layout="fill" obtiene la position: absolute; del marco, por lo que podemos limitar el tamaño colocando un contenedor con posición: relativa;.

Agreguemos una nueva clase a nuestro TeaListItem.module.css :

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

Y luego agreguemos className={styles.ImageContainer} en el <div> que contiene nuestra <Image> . Uso nombres relativamente "simples" como ImageContainer porque estamos dentro de un módulo CSS, por lo que no tenemos que preocuparnos por entrar en conflicto con el estilo exterior.

Finalmente, queremos agregar un poco de relleno a los lados del texto, así que agreguemos una última clase y confiemos en las variables de espaciado que configuramos como tokens de diseño:

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

Podemos agregar esta clase al <div> que contiene nuestro nombre y descripción. Ahora, nuestras tarjetas no se ven tan mal:

Se muestran tarjetas para 3 tés diferentes que se agregaron como datos iniciales. Tienen imágenes, nombres y descripciones. Actualmente aparecen en una lista vertical sin espacio entre ellos.
Se muestran tarjetas para 3 tés diferentes que se agregaron como datos iniciales. Tienen imágenes, nombres y descripciones. Actualmente aparecen en una lista vertical sin espacio entre ellos. (Vista previa grande)

Combinando estilo global y local

A continuación, queremos que nuestras tarjetas se muestren en un diseño de cuadrícula. En este caso, estamos justo en la frontera entre los estilos local y global. Ciertamente podríamos codificar nuestro diseño directamente en el componente TeaList . Pero también podría imaginar que tener una clase de utilidad que convierta una lista en un diseño de cuadrícula podría ser útil en varios otros lugares.

Adoptemos el enfoque global aquí y agreguemos una nueva clase de utilidad en nuestros estilos/utilidades.css :

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

Ahora, podemos agregar la clase .grid en cualquier lista y obtendremos un diseño de cuadrícula que responde automáticamente. También podemos cambiar la propiedad personalizada --min-item-width (por defecto 30ch ) para cambiar el ancho mínimo de cada elemento.

Nota : ¡Recuerde pensar en propiedades personalizadas como accesorios! Si esta sintaxis no le resulta familiar, puede consultar "Cuadrícula CSS intrínsecamente receptiva con minmax() y min() " de Chris Coyier.

Como hemos escrito este estilo globalmente, no requiere ninguna fantasía para agregar className="grid" a nuestro componente TeaList . Pero digamos que queremos combinar este estilo global con alguna tienda local adicional. Por ejemplo, queremos incorporar un poco más de la "estética del té" y hacer que todas las demás cartas tengan un fondo verde. Todo lo que tendríamos que hacer es crear un nuevo archivo components/TeaList/TeaList.module.css :

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

¿Recuerda cómo creamos una propiedad --color custom en nuestro componente TeaListItem ? Bueno, ahora podemos configurarlo en circunstancias específicas. Tenga en cuenta que aún podemos usar selectores secundarios dentro de los módulos CSS, y no importa que estemos seleccionando un elemento que tiene un estilo dentro de un módulo diferente. Por lo tanto, también podemos usar nuestros estilos de componentes locales para afectar los componentes secundarios. ¡Esta es una característica más que un error, ya que nos permite aprovechar la cascada de CSS ! Si tratáramos de replicar este efecto de alguna otra manera, probablemente terminaríamos con algún tipo de sopa de JavaScript en lugar de tres líneas de CSS.

Entonces, ¿cómo podemos mantener la clase .grid global en nuestro componente TeaList mientras agregamos la clase .TeaList local? Aquí es donde la sintaxis puede volverse un poco extraña porque tenemos que acceder a nuestra clase .TeaList desde el módulo CSS haciendo algo como style.TeaList .

Una opción sería usar la interpolación de cadenas para obtener algo como:

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

En este pequeño caso, esto podría ser lo suficientemente bueno. Si estamos mezclando y emparejando más clases, encuentro que esta sintaxis hace que mi cerebro explote un poco, por lo que a veces optaré por usar la biblioteca de nombres de clase. En este caso, terminamos con una lista más sensata:

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

Ahora, hemos terminado nuestra página de Tienda y hemos hecho que nuestro componente TeaList aproveche los estilos global y local.

Nuestras tarjetas de té ahora se muestran en una cuadrícula. Los enteros pares son de color verde, mientras que las entradas impares son de color blanco.
Nuestras tarjetas de té ahora se muestran en una cuadrícula. Los enteros pares son de color verde, mientras que las entradas impares son de color blanco. (Vista previa grande)

Un acto de equilibrio

Ahora hemos construido nuestra tienda de té usando solo CSS simple para manejar el estilo. Es posible que haya notado que no tuvimos que pasar mucho tiempo lidiando con configuraciones personalizadas de Webpack, instalando bibliotecas externas, etc. Esto se debe a que los patrones que hemos usado funcionan con Next.js de fábrica. Además, fomentan las mejores prácticas de CSS y encajan naturalmente en la arquitectura del marco Next.js.

Nuestra organización de CSS constaba de cuatro piezas clave:

  1. fichas de diseño,
  2. estilos globales,
  3. Clases de utilidad,
  4. Estilos de componentes.

A medida que continuamos construyendo nuestro sitio, nuestra lista de tokens de diseño y clases de utilidad crecerá. Cualquier estilo que no tenga sentido agregar como una clase de utilidad, podemos agregarlo a los estilos de componentes usando módulos CSS. Como resultado, podemos encontrar un equilibrio continuo entre las preocupaciones de estilo locales y globales. También podemos generar código CSS intuitivo y de alto rendimiento que crece de forma natural junto con nuestro sitio Next.js.