Estilo global versus local no Next.js

Publicados: 2022-03-10
Resumo rápido ↬ Next.js tem opiniões fortes sobre como organizar JavaScript, mas não CSS. Como podemos desenvolver padrões que encorajem as melhores práticas de CSS enquanto também seguem a lógica do framework? A resposta é surpreendentemente simples — escrever CSS bem estruturado que equilibre as preocupações de estilo global e local.

Tive uma ótima experiência usando o Next.js para gerenciar projetos complexos de front-end. Next.js é opinativo sobre como organizar código JavaScript, mas não tem opiniões internas sobre como organizar CSS.

Depois de trabalhar dentro da estrutura, encontrei uma série de padrões organizacionais que, acredito, estão em conformidade com as filosofias orientadoras do Next.js e exercem as melhores práticas de CSS. Neste artigo, vamos construir um site (uma loja de chá!) juntos para demonstrar esses padrões.

Nota : Você provavelmente não precisará de experiência prévia com Next.js, embora seja bom ter um conhecimento básico de React e estar aberto para aprender algumas novas técnicas de CSS.

Escrevendo CSS “antiquado”

Ao examinar o Next.js pela primeira vez, podemos ser tentados a considerar o uso de algum tipo de biblioteca CSS-in-JS. Embora possa haver benefícios dependendo do projeto, CSS-in-JS apresenta muitas considerações técnicas. Requer o uso de uma nova biblioteca externa, que aumenta o tamanho do pacote. O CSS-in-JS também pode ter um impacto no desempenho, causando renderizações e dependências adicionais no estado global.

Leitura recomendada : “ The Unseen Performance Costs Of Modern CSS-in-JS Libraries In React Apps)” por Aggelos Arvanitakis

Além disso, o objetivo de usar uma biblioteca como Next.js é renderizar ativos estaticamente sempre que possível, então não faz muito sentido escrever JS que precisa ser executado no navegador para gerar CSS.

Há algumas questões que devemos considerar ao organizar o estilo no Next.js:

Como podemos nos encaixar nas convenções/melhores práticas da estrutura?

Como podemos equilibrar preocupações de estilo “globais” (fontes, cores, layouts principais e assim por diante) com os “locais” (estilos relativos a componentes individuais)?

A resposta que encontrei para a primeira pergunta é simplesmente escrever o bom e velho CSS . O Next.js não apenas suporta isso sem nenhuma configuração adicional; ele também produz resultados com desempenho e estáticos.

Para resolver o segundo problema, adoto uma abordagem que pode ser resumida em quatro partes:

  1. Fichas de design
  2. Estilos globais
  3. Classes de utilidade
  4. Estilos de componentes

Estou em dívida com a ideia de Andy Bell de CUBE CSS (“Composição, Utilidade, Bloco, Exceção”) aqui. Se você nunca ouviu falar desse princípio organizacional antes, eu recomendo verificar seu site oficial ou recurso no Smashing Podcast. Um dos princípios que tiraremos do CUBE CSS é a ideia de que devemos abraçar em vez de temer a cascata CSS. Vamos aprender essas técnicas aplicando-as a um projeto de site.

Começando

Vamos construir uma loja de chá porque, bem, chá é gostoso. Começaremos executando yarn create next-app para criar um novo projeto Next.js. Em seguida, removeremos tudo no styles/ directory (é todo código de exemplo).

Nota : Se você quiser acompanhar o projeto finalizado, você pode conferir aqui.

Fichas de design

Em praticamente qualquer configuração de CSS, há um claro benefício em armazenar todos os valores compartilhados globalmente em variáveis ​​. Se um cliente pede uma mudança de cor, implementar a mudança é uma linha única, em vez de uma enorme bagunça de encontrar e substituir. Consequentemente, uma parte importante de nossa configuração CSS Next.js armazenará todos os valores de todo o site como tokens de design .

Usaremos propriedades personalizadas CSS incorporadas para armazenar esses tokens. (Se você não estiver familiarizado com esta sintaxe, você pode conferir “A Strategy Guide To CSS Custom Properties”.) Devo mencionar que (em alguns projetos) optei por usar variáveis ​​SASS/SCSS para este propósito. Não encontrei nenhuma vantagem real, então geralmente incluo SASS em um projeto se achar que preciso de outros recursos SASS (mix-ins, iteração, importação de arquivos e assim por diante). As propriedades personalizadas de CSS, por outro lado, também funcionam com a cascata e podem ser alteradas ao longo do tempo, em vez de compilar estaticamente. Então, por hoje, vamos ficar com CSS simples .

Em nosso diretório styles/ , vamos criar um novo arquivo 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; }

Claro, essa lista pode e vai crescer com o tempo. Uma vez que adicionamos este arquivo, precisamos pular para nosso arquivo pages/_app.jsx , que é o layout principal de todas as nossas páginas, e adicionar:

 import '../styles/design_tokens.css'

Gosto de pensar nos tokens de design como a cola que mantém a consistência em todo o projeto. Faremos referência a essas variáveis ​​em escala global, bem como em componentes individuais, garantindo uma linguagem de design unificada.

Mais depois do salto! Continue lendo abaixo ↓

Estilos Globais

Em seguida, vamos adicionar uma página ao nosso site! Vamos entrar no arquivo pages/index.jsx (esta é nossa página inicial). Vamos excluir todo o clichê e adicionar 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> }

Infelizmente, vai parecer bem simples, então vamos definir alguns estilos globais para elementos básicos , por exemplo, tags <h1> . (Gosto de pensar nesses estilos como “padrões globais razoáveis”.) Podemos substituí-los em casos específicos, mas eles são um bom palpite sobre o que desejaremos se não o fizermos.

Vou colocar isso no arquivo styles/globals.css (que vem por padrão do 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%; }

Claro, esta versão é bastante básica, mas meu arquivo globals.css geralmente não precisa ficar muito grande. Aqui, estilizo elementos HTML básicos (títulos, corpo, links e assim por diante). Não há necessidade de envolver esses elementos em componentes React ou adicionar classes constantemente apenas para fornecer estilo básico.

Eu também incluo quaisquer redefinições de estilos de navegador padrão . Ocasionalmente, terei algum estilo de layout para todo o site para fornecer um “rodapé fixo”, por exemplo, mas eles só pertencem aqui se todas as páginas compartilharem o mesmo layout. Caso contrário, ele precisará ser delimitado dentro de componentes individuais.

Eu sempre incluo algum tipo de estilo :focus para indicar claramente elementos interativos para usuários de teclado quando focados. É melhor torná-lo parte integrante do DNA de design do site!

Agora, nosso site está começando a se formar:

Imagem do site do trabalho em andamento. O plano de fundo da página agora é azul escuro e o título "Chás calmantes" é verde. O site não tem layout/espaçamento e, portanto, se estende completamente até a largura da janela do navegador.
Imagem do site do trabalho em andamento. O plano de fundo da página agora é azul escuro e o título "Chás calmantes" é verde. O site não tem layout/espaçamento e, portanto, se estende completamente até a largura da janela do navegador. (Visualização grande)

Classes de utilidade

Uma área em que nossa página inicial certamente poderia melhorar é que o texto atualmente sempre se estende para os lados da tela, então vamos limitar sua largura. Precisamos desse layout nesta página, mas imagino que possamos precisar dele em outras páginas também. Este é um ótimo caso de uso para uma classe de utilitário!

Eu tento usar classes utilitárias com moderação ao invés de como um substituto para apenas escrever CSS. Meus critérios pessoais para quando faz sentido adicionar um a um projeto são:

  1. Eu preciso disso repetidamente;
  2. Ele faz uma coisa bem;
  3. Aplica-se a uma variedade de componentes ou páginas diferentes.

Acho que este caso atende a todos os três critérios, então vamos criar um novo arquivo CSS styles/utilities.css e adicionar:

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

Então vamos adicionar import '../styles/utilities.css' para nossas páginas/_app.jsx . Por fim, vamos alterar a tag <main> em nosso pages/index.jsx para <main className="lockup"> .

Agora, nossa página está se unindo ainda mais. Como usamos a propriedade max-width , não precisamos de consultas de mídia para tornar nosso layout responsivo para dispositivos móveis. E, como usamos a unidade de medida ch — que equivale a aproximadamente a largura de um caractere — nosso dimensionamento é dinâmico para o tamanho da fonte do navegador do usuário.

O mesmo site de antes, mas agora o texto fica preso no meio e não fica muito largo
O mesmo site de antes, mas agora o texto fica preso no meio e não fica muito largo. (Visualização grande)

À medida que nosso site cresce, podemos continuar adicionando mais classes de utilidade. Eu adoto uma abordagem bastante utilitária aqui: se estou trabalhando e acho que preciso de outra classe para uma cor ou algo assim, eu a adiciono. Eu não adiciono todas as classes possíveis sob o sol - isso aumentaria o tamanho do arquivo CSS e tornaria meu código confuso. Às vezes, em projetos maiores, gosto de dividir as coisas em um diretório styles/utilities/ com alguns arquivos diferentes; vai de acordo com as necessidades do projeto.

Podemos pensar nas classes utilitárias como nosso kit de ferramentas de comandos de estilo comuns e repetidos que são compartilhados globalmente. Eles ajudam a nos impedir de reescrever constantemente o mesmo CSS entre diferentes componentes.

Estilos de componentes

Nós terminamos nossa homepage por enquanto, mas ainda precisamos construir um pedaço do nosso site: a loja online. Nosso objetivo aqui será exibir uma grade de cartões de todos os chás que queremos vender , então precisaremos adicionar alguns componentes ao nosso site.

Vamos começar adicionando uma nova página em pages/shop.jsx :

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

Então, vamos precisar de alguns chás para exibir. Incluiremos um nome, uma descrição e uma imagem (no diretório public/) para cada chá:

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

Nota : Este não é um artigo sobre busca de dados, então pegamos o caminho mais fácil e definimos um array no início do arquivo.

Em seguida, precisaremos definir um componente para exibir nossos chás. Vamos começar criando um diretório components/ (Next.js não faz isso por padrão). Então, vamos adicionar um diretório components/TeaList . Para qualquer componente que acabe precisando de mais de um arquivo, costumo colocar todos os arquivos relacionados dentro de uma pasta. Isso evita que nossos components/ pasta fiquem inavegáveis.

Agora, vamos adicionar nosso arquivo 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

O propósito deste componente é iterar sobre nossos chás e mostrar um item de lista para cada um, então agora vamos definir nosso 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

Observe que estamos usando o componente de imagem integrado do Next.js. Eu configurei o atributo alt para uma string vazia porque as imagens são puramente decorativas neste caso; queremos evitar sobrecarregar os usuários de leitores de tela com longas descrições de imagens aqui.

Finalmente, vamos criar um arquivo components/TeaList/index.js , para que nossos componentes sejam fáceis de importar externamente:

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

E então, vamos conectar tudo adicionando import TeaList de ../components/TeaList e um <TeaList teas={teas} /> à nossa página Shop. Agora, nossos chás aparecerão em uma lista, mas não será tão bonito.

Colocando estilo com componentes através de módulos CSS

Vamos começar estilizando nossos cartões (o componente TeaListLitem ). Agora, pela primeira vez em nosso projeto, vamos querer adicionar um estilo específico para apenas um componente. Vamos criar um novo arquivo components/TeaList/TeaListItem.module.css .

Você pode estar se perguntando sobre o módulo na extensão do arquivo. Este é um módulo CSS . Next.js suporta módulos CSS e inclui uma boa documentação sobre eles. Quando escrevemos um nome de classe de um módulo CSS como .TeaListItem , ele será automaticamente transformado em algo mais parecido com . TeaListItem_TeaListItem__TFOk_ . TeaListItem_TeaListItem__TFOk_ com um monte de caracteres extras adicionados. Conseqüentemente, podemos usar qualquer nome de classe que quisermos sem nos preocuparmos com o conflito com outros nomes de classe em outros lugares do nosso site.

Outra vantagem dos módulos CSS é o desempenho. Next.js inclui um recurso de importação dinâmica. next/dynamic nos permite carregar componentes lentamente para que seu código seja carregado apenas quando necessário, em vez de adicionar ao tamanho do pacote inteiro. Se importarmos os estilos locais necessários para componentes individuais, os usuários também poderão carregar lentamente o CSS para componentes importados dinamicamente . Para projetos grandes, podemos optar por carregar lentamente partes significativas de nosso código e apenas carregar o JS/CSS mais necessário antecipadamente. Como resultado, geralmente acabo criando um novo arquivo de módulo CSS para cada novo componente que precisa de estilo local.

Vamos começar adicionando alguns estilos iniciais ao nosso arquivo:

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

Em seguida, podemos importar o estilo de ./TeaListItem.module.css em nosso componente TeaListitem . A variável de estilo vem como um objeto JavaScript, então podemos acessar este estilo de style.TeaListItem.

Nota : O nome da nossa classe não precisa ser capitalizado. Descobri que uma convenção de nomes de classes em maiúsculas dentro de módulos (e em minúsculas fora) diferencia nomes de classes locais vs. globais visualmente.

Então, vamos pegar nossa nova classe local e atribuí-la ao <li> em nosso componente TeaListItem :

 <li className={style.TeaListComponent}>

Você pode estar se perguntando sobre a linha de cor de fundo (ou seja var(--color, var(--off-white)); ). O que esse snippet significa é que , por padrão, o plano de fundo será nosso valor --off-white . Mas, se definirmos uma propriedade personalizada --color em um cartão, ele substituirá e escolherá esse valor.

A princípio, queremos que todas as nossas cartas sejam --off-white , mas podemos querer alterar o valor de cartas individuais posteriormente. Isso funciona de maneira muito semelhante aos adereços no React. Podemos definir um valor padrão, mas criar um slot onde podemos escolher outros valores em circunstâncias específicas. Então, encorajo-nos a pensar em propriedades personalizadas CSS como a versão CSS de props .

O estilo ainda não ficará ótimo porque queremos garantir que as imagens permaneçam dentro de seus contêineres. O componente Image do Next.js com o prop layout="fill" obtém position: absolute; do framework, então podemos limitar o tamanho colocando em um container com position: relative;.

Vamos adicionar uma nova classe ao nosso TeaListItem.module.css :

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

E então vamos adicionar className={styles.ImageContainer} no <div> que contém nosso <Image> . Eu uso nomes relativamente “simples” como ImageContainer porque estamos dentro de um módulo CSS, então não precisamos nos preocupar em entrar em conflito com o estilo externo.

Finalmente, queremos adicionar um pouco de preenchimento nas laterais do texto, então vamos adicionar uma última classe e confiar nas variáveis ​​de espaçamento que configuramos como tokens de design:

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

Podemos adicionar esta classe ao <div> que contém nosso nome e descrição. Agora, nossos cartões não parecem tão ruins:

Os cartões estão sendo exibidos para 3 chás diferentes que foram adicionados como dados iniciais. Eles têm imagens, nomes e descrições. Atualmente, eles aparecem em uma lista vertical sem espaço entre eles.
Os cartões estão sendo exibidos para 3 chás diferentes que foram adicionados como dados iniciais. Eles têm imagens, nomes e descrições. Atualmente, eles aparecem em uma lista vertical sem espaço entre eles. (Visualização grande)

Combinando estilo global e local

Em seguida, queremos que nossos cartões sejam exibidos em um layout de grade. Neste caso, estamos apenas na fronteira entre estilos locais e globais. Certamente poderíamos codificar nosso layout diretamente no componente TeaList . Mas, eu também poderia imaginar que ter uma classe utilitária que transforma uma lista em um layout de grade poderia ser útil em vários outros lugares.

Vamos usar a abordagem global aqui e adicionar uma nova classe de utilitário em nosso 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); }

Agora, podemos adicionar a classe .grid em qualquer lista e obteremos um layout de grade responsivo automaticamente. Também podemos alterar a propriedade personalizada --min-item-width (por padrão 30ch ) para alterar a largura mínima de cada elemento.

Nota : Lembre-se de pensar em propriedades personalizadas como adereços! Se essa sintaxe não for familiar, você pode conferir “Intrinsically Responsive CSS Grid With minmax() And min() ” de Chris Coyier.

Como escrevemos esse estilo globalmente, não é necessário nenhum capricho para adicionar className="grid" ao nosso componente TeaList . Mas, digamos que queremos combinar esse estilo global com alguma loja local adicional. Por exemplo, queremos trazer um pouco mais da “estética do chá” e fazer com que todas as outras cartas tenham um fundo verde. Tudo o que precisamos fazer é criar um novo arquivo components/TeaList/TeaList.module.css :

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

Lembra como criamos uma propriedade --color custom em nosso componente TeaListItem ? Bem, agora podemos configurá-lo em circunstâncias específicas. Observe que ainda podemos usar seletores filho dentro de módulos CSS e não importa se estamos selecionando um elemento com estilo dentro de um módulo diferente. Portanto, também podemos usar nossos estilos de componentes locais para afetar os componentes filhos. Este é um recurso e não um bug, pois nos permite tirar proveito da cascata CSS ! Se tentássemos replicar esse efeito de outra forma, provavelmente acabaríamos com algum tipo de sopa de JavaScript em vez de três linhas de CSS.

Então, como podemos manter a classe .grid global em nosso componente TeaList enquanto também adicionamos a classe .TeaList local? É aqui que a sintaxe pode ficar um pouco estranha porque temos que acessar nossa classe .TeaList fora do módulo CSS fazendo algo como style.TeaList .

Uma opção seria usar a interpolação de string para obter algo como:

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

Neste caso pequeno, isso pode ser bom o suficiente. Se estivermos misturando e combinando mais classes, acho que essa sintaxe faz meu cérebro explodir um pouco, então às vezes optarei por usar a biblioteca de nomes de classes. Nesse caso, acabamos com uma lista de aparência mais sensata:

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

Agora, finalizamos nossa página Shop e fizemos nosso componente TeaList aproveitar os estilos global e local.

Nossos cartões de chá agora são exibidos em uma grade. Os inteiros pares são coloridos em verde, enquanto as entradas ímpares são brancas.
Nossos cartões de chá agora são exibidos em uma grade. Os inteiros pares são coloridos em verde, enquanto as entradas ímpares são brancas. (Visualização grande)

Um ato de equilíbrio

Agora construímos nossa loja de chá usando apenas CSS simples para lidar com o estilo. Você deve ter notado que não tivemos que gastar muito tempo lidando com configurações personalizadas do Webpack, instalando bibliotecas externas e assim por diante. Isso ocorre porque os padrões que usamos funcionam com o Next.js pronto para uso. Além disso, eles incentivam as melhores práticas de CSS e se encaixam naturalmente na arquitetura do framework Next.js.

Nossa organização CSS consistia em quatro peças principais:

  1. Símbolos de design,
  2. Estilos globais,
  3. Aulas de utilidade,
  4. Estilos de componentes.

À medida que continuamos a construir nosso site, nossa lista de tokens de design e classes de utilidade aumentará. Qualquer estilo que não faça sentido adicionar como uma classe utilitária, podemos adicionar estilos de componentes usando módulos CSS. Como resultado, podemos encontrar um equilíbrio contínuo entre as preocupações de estilo locais e globais. Também podemos gerar código CSS intuitivo e de alto desempenho que cresce naturalmente junto com nosso site Next.js.