CSS: Classes utilitárias e “Separation of Concerns”
Evoluindo seu CSS para uma abordagem funcional
Nos últimos anos, a forma como escrevo CSS passou de uma abordagem “semântica” para algo muito mais parecido com o que é frequentemente chamado de “CSS funcional”.
Escrever CSS dessa maneira pode provocar uma reação visceral em muitos desenvolvedores. Eu gostaria de explicar como cheguei a este ponto e compartilhar algumas das lições e insights que aprendi ao longo do caminho.
Fase 1: CSS “semântico”
Uma das melhores práticas que você ouvirá quando estiver tentando aprender CSS é “separação de interesses”.
A ideia é que o seu HTML contenha apenas informações sobre o seu conteúdo , e todas as suas decisões de estilo devem ser feitas no seu CSS.
Dê uma olhada neste HTML:
<p class="text-center">
Hello there!
</p>
Veja essa classe .text-center
? A centralização de texto é uma decisão de design, portanto, esse código viola a "separação de interesses" porque permitimos que as informações de estilo sejam inseridas em nosso HTML.
Em vez disso, a abordagem recomendada é fornecer nomes de classe a seus elementos com base em seu conteúdo e usar essas classes como ganchos em seu CSS para estilizar sua marcação:
<style>
.greeting {
text-align: center;
}
</style>
<p class="greeting">
Hello there!
</p>
O exemplo típico dessa abordagem sempre foi o CSS Zen Garden. Um site criado para mostrar que, se você “separar suas preocupações”, poderá reformular completamente um site apenas trocando a folha de estilo.
Meu fluxo de trabalho parecia algo assim:
- Escreva a marcação que eu precisava para uma nova interface do usuário (um card com uma bio de um autor):
<div>
<img src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt="">
<div>
<h2>Adam Wathan</h2>
<p>
Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
</p>
</div>
</div>
- Adicione uma ou duas classes descritivas com base no conteúdo:
- <div>
+ <div class="author-bio">
<img src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt="">
<div>
<h2>Adam Wathan</h2>
<p>
Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
</p>
</div>
</div>
- Use essas classes como “ganchos” no seu CSS / Less / Sass para estilizar a nova marcação:
.author-bio {
background-color: white;
border: 1px solid hsl(0,0%,85%);
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
> img {
display: block;
width: 100%;
height: auto;
}
> div {
padding: 1rem;
> h2 {
font-size: 1.25rem;
color: rgba(0,0,0,0.8);
}
> p {
font-size: 1rem;
color: rgba(0,0,0,0.75);
line-height: 1.5;
}
}
}
Aqui está uma demonstração do resultado final:
Essa abordagem intuitivamente fez sentido para mim e, por um tempo, foi assim que escrevi HTML e CSS.
Eventualmente, porém, algo começou a ficar um pouco fora.
Eu tinha “separado minhas preocupações”, mas ainda havia um acoplamento muito óbvio entre meu CSS e meu HTML. Na maioria das vezes meu CSS era como um espelho para a minha marcação: refletindo perfeitamente minha estrutura HTML com seletores CSS aninhados.
Minha marcação não se preocupava com decisões de estilo, mas meu CSS estava muito preocupado com minha estrutura de marcação.
Talvez minhas preocupações não estivessem tão separadas, afinal.
Fase 2: Desacoplamento de estilos da estrutura
Depois de procurar uma solução para esse acoplamento, comecei a encontrar mais e mais recomendações para adicionar mais classes à sua marcação, para que você pudesse segmentá-las diretamente, mantendo a especificidade do seletor baixa e tornando seu CSS menos dependente de sua estrutura particular do DOM.
A metodologia mais conhecida que defende essa ideia é o Block Element Modifier, ou BEM, abreviado.
Tomando uma abordagem do tipo BEM, a marcação para a nossa bio do autor pode ser mais ou menos assim:
<div class="author-bio">
<img class="author-bio__image" src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt="">
<div class="author-bio__content">
<h2 class="author-bio__name">Adam Wathan</h2>
<p class="author-bio__body">
Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
</p>
</div>
</div>
Nosso CSS ficaria assim:
.author-bio {
background-color: white;
border: 1px solid hsl(0,0%,85%);
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
}
.author-bio__image {
display: block;
width: 100%;
height: auto;
}
.author-bio__content {
padding: 1rem;
}
.author-bio__name {
font-size: 1.25rem;
color: rgba(0,0,0,0.8);
}
.author-bio__body {
font-size: 1rem;
color: rgba(0,0,0,0.75);
line-height: 1.5;
}
Isso pareceu uma grande melhoria para mim. Minha marcação ainda era “semântica” e não continha nenhuma decisão de estilo. Agora meu CSS parecia separado da minha estrutura de marcação, com a vantagem adicional de evitar especificidade de seletor desnecessária.
Mas então me deparei com um dilema.
Lidando com componentes semelhantes
Digamos que eu precise adicionar um novo recurso ao site: exibindo uma visualização de um artigo em um layout de cartão.
Digamos que este card de visualização do artigo tenha uma imagem completa no topo, uma seção de conteúdo preenchido abaixo, um título em negrito e um texto do corpo menor.
Digamos que parecesse exatamente como uma biografia do autor.
Qual é a melhor maneira de lidar com isso enquanto ainda separamos nossas preocupações?
Não podemos aplicar nossas classes de .author-bio
; isso não seria semântico. Então, definitivamente precisamos fazer um componente .article-preview
.
Veja como a nossa marcação pode ser:
<div class="article-preview">
<img class="article-preview__image" src="https://i.vimeocdn.com/video/585037904_1280x720.webp" alt="">
<div class="article-preview__content">
<h2 class="article-preview__title">Stubbing Eloquent Relations for Faster Tests</h2>
<p class="article-preview__body">
In this quick blog post and screencast, I share a trick I use to speed up tests that use Eloquent relationships but don't really depend on database functionality.
</p>
</div>
</div>
Mas como devemos lidar com o CSS nesse caso?
Opção 1: Duplicar os estilos
Uma abordagem seria duplicar os estilos do nosso .author-bio
e renomear as classes:
.article-preview {
background-color: white;
border: 1px solid hsl(0,0%,85%);
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
}
.article-preview__image {
display: block;
width: 100%;
height: auto;
}
.article-preview__content {
padding: 1rem;
}
.article-preview__title {
font-size: 1.25rem;
color: rgba(0,0,0,0.8);
}
.article-preview__body {
font-size: 1rem;
color: rgba(0,0,0,0.75);
line-height: 1.5;
}
Isso funciona, mas é claro que não é muito DRY (Don’t Repeat Yourself. Isso facilita que esses componentes modifiers de maneiras ligeiramente diferentes (talvez um preenchimento diferente, ou cor de fonte) que pode levar a uma inconsistência no design.
Opcão 2: Utilizando @extend
Outra abordagem é usar o @extend
do seu pré-processador; Deixando você pegar carona nos estilos já definidos em nosso .author-bio
:
.article-preview {
@extend .author-bio;
}
.article-preview__image {
@extend .author-bio__image;
}
.article-preview__content {
@extend .author-bio__content;
}
.article-preview__title {
@extend .author-bio__name;
}
.article-preview__body {
@extend .author-bio__body;
}
Usar @extend
geralmente não é recomendado, mas isso de lado, parece que isso resolve nosso problema, certo?
Removemos a duplicação em nosso CSS e nossa marcação ainda está livre de decisões de estilo.
Mas vamos examinar mais uma opção…
Opção 3: Criar um componente agnóstico de conteúdo
Nossos .author-bio
e .article-preview
não têm nada em comum a partir de uma perspectiva "semântica". Uma é a biografia de um autor, a outra é uma prévia de um artigo.
Mas, como já vimos, eles têm muito em comum do ponto de vista do design.
Então, poderíamos criar um novo componente genérico, sobre o que eles não têm em comum, e reutilizar esse componente para ambos os tipos de conteúdo.
Vamos chamar isso de .media-card
O CSS fica:
.media-card {
background-color: white;
border: 1px solid hsl(0,0%,85%);
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
}
.media-card__image {
display: block;
width: 100%;
height: auto;
}
.media-card__content {
padding: 1rem;
}
.media-card__title {
font-size: 1.25rem;
color: rgba(0,0,0,0.8);
}
.media-card__body {
font-size: 1rem;
color: rgba(0,0,0,0.75);
line-height: 1.5;
}
Revisitando o componente .author-bio
:
<div class="media-card">
<img class="media-card__image" src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt="">
<div class="media-card__content">
<h2 class="media-card__title">Adam Wathan</h2>
<p class="media-card__body">
Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
</p>
</div>
</div>
E o componente .article-preview
:
<div class="media-card">
<img class="media-card__image" src="https://i.vimeocdn.com/video/585037904_1280x720.webp" alt="">
<div class="media-card__content">
<h2 class="media-card__title">Stubbing Eloquent Relations for Faster Tests</h2>
<p class="media-card__body">
In this quick blog post and screencast, I share a trick I use to speed up tests that use Eloquent relationships but don't really depend on database functionality.
</p>
</div>
</div>
Essa abordagem também remove a duplicação de nosso CSS, mas não estamos “misturando preocupações” agora?
Nossa marcação, de repente, sabe que queremos que ambas as partes do conteúdo sejam estilizadas como cartões de mídia. E se quiséssemos mudar a aparência da biografia do autor sem alterar a aparência da visualização do artigo?
Antes, poderíamos apenas abrir nossa folha de estilo e escolher novos estilos para qualquer um dos dois componentes. Agora precisamos editar o HTML! Blasfêmia!
Mas vamos pensar no outro lado por um minuto.
E se precisássemos adicionar um novo tipo de conteúdo que também precisasse do mesmo estilo?
Usando uma abordagem “semântica”, precisaríamos escrever o novo HTML, adicionar algumas classes específicas de conteúdo como “ganchos” de estilo, abrir nossa folha de estilo, criar um novo componente CSS para o novo tipo de conteúdo e aplicar os estilos compartilhados, através de duplicação ou usando @extend
ou um @mixin
.
Usando um componente agnóstico, como .media-card
, tudo o que precisamos escrever é o novo HTML; não teríamos que abrir o CSS para nada.
Bom…se estamos realmente “misturando preocupações”, não precisaríamos fazer mudanças em vários lugares?
“Separation of Concerns” é uma agulha no palheiro
Quando você pensa sobre a relação entre HTML e CSS em termos de “separação de interesses”, é muito preto e branco.
Você tem uma separação de interesses (bom!), ou não (mal!).
Este não é o caminho certo para se pensar em HTML e CSS.
Ao invés disso, pense na direção da dependência.
Existem duas maneiras de escrever HTML e CSS:
- CSS que depende do HTML (“Separação de Preocupações”)
Nomear suas classes com base em seu conteúdo (como .author-bio
) trata seu HTML como uma dependência de seu CSS.
O HTML é independente; não importa como você o faz parecer, apenas expõe ganchos como .author-bio
, que é o que o HTML controla.
Seu CSS, por outro lado, não é independente; ele precisa saber quais classes seu HTML decidiu expor e precisa direcionar essas classes para estilizar o HTML.
Nesse modelo, seu HTML é reutilizável, mas seu CSS não é reutilizável.
- HTML que depende do CSS (“Misturando Preocupações”)
Nomear suas classes de maneira independente, depois que alguns padrões forem definidos em sua interface do usuário (como .media-card
) tratam seu CSS como uma dependência de seu HTML.
O CSS é independente; não importa qual conteúdo está sendo aplicado, apenas expõe um conjunto de blocos de construção que você pode aplicar à sua marcação.
Seu HTML não é independente; Ele está fazendo uso de classes que foram fornecidas pelo CSS, e precisa saber quais classes existem para que elas sejam combinadas para obter o design desejado.
Nesse modelo, seu CSS é reutilizável, mas seu HTML não pode ser reestilizado.
O CSS Zen Garden adota a primeira abordagem, enquanto as estruturas de UI, como Bootstrap ou Bulma, adotam a segunda abordagem.
Nenhum deles é inerentemente “errado”; é apenas uma decisão tomada com base no que é mais importante para você em um contexto específico.
Para o projeto em que você está trabalhando, o que seria mais valioso: HTML reutilizável ou CSS reutilizável?
Escolhendo reutilização
O ponto de virada para mim veio quando li “Sobre semântica HTML e arquitetura front-end, de Nicolas Gallagher”.
Eu não vou reiterar todos os pontos aqui, mas posso dizer que eu saí daquele post totalmente convencido de que a otimização para CSS reutilizável seria a escolha certa para os tipos de projetos que eu trabalho.
Fase 3: Componentes CSS independentes de conteúdo
Meu objetivo neste momento é evitar explicitamente criar classes baseadas em meu conteúdo, ao invés de tentar nomear tudo de uma forma que fosse o mais reutilizável possível.
Isso resultou em nomes de classes como:
.card
.btn
,.bth--primary
,.btn--secondary
.badge
.card-list
,.card-list-item
.img--round
.modal-form
,.modal-form-section
E assim por diante…
Eu notei outra coisa quando comecei a me concentrar na criação de classes reutilizáveis:
Quanto mais componentes você faz, ou quanto mais específico é um componente, mais difícil é reutilizá-lo.
Aqui está um exemplo intuitivo.
Digamos que estamos criando um formulário com algumas seções e um botão de envio na parte inferior.
Se pensarmos em todo o conteúdo do formulário como parte de um componente .stacked-form
, poderíamos dar ao botão de envio uma classe como .stacked-form__button
:
<form class="stacked-form" action="#">
<div class="stacked-form__section">
<!-- ... -->
</div>
<div class="stacked-form__section">
<!-- ... -->
</div>
<div class="stacked-form__section">
<button class="stacked-form__button">Submit</button>
</div>
</form>
Mas talvez, outro botão em nosso site, que não faz parte de um formulário, irá precisar ser estilizado da mesma maneira.
Usar .stacked-form__button
nesse botão não faria muito sentido; não faz parte de um formulário.
Ambos os botões são ações primárias em suas respectivas páginas, então e se nós nomearmos o botão com base no que os componentes têm em comum e o chamarem .btn--primary
, removendo o prefixo .stacked-form__
:
<form class="stacked-form" action="#">
<!-- ... -->
<div class="stacked-form__section">
- <button class="stacked-form__button">Submit</button>
+ <button class="btn btn--primary">Submit</button>
</div>
</form>
Agora, queremos que esse formulário tenha um estilo de cartão flutuante.
Uma abordagem seria criar um modificador e aplicá-lo a este formulário:
- <form class="stacked-form" action="#">
+ <form class="stacked-form stacked-form--card" action="#">
<!-- ... -->
</form>
Mas se já temos um .card
, por que não compomos essa nova interface do usuário usando o componente existente?
+ <div class="card">
<form class="stacked-form" action="#">
<!-- ... -->
</form>
+ </div>
Ao adotar essa abordagem, temos um .card
que pode ser o componente pai de qualquer conteúdo, e um .stacked-form
agnóstico, pode ser usado dentro de qualquer container.
Estamos obtendo mais reutilização de nossos componentes e não precisamos escrever nenhum novo CSS.
Composição sobre subcomponentes
Digamos que queremos adicionar outro botão ao final do nosso formulário, e que fosse um pouco diferente do botão existente:
<form class="stacked-form" action="#">
<!-- ... -->
<div class="stacked-form__section">
<button class="btn btn--secondary">Cancel</button>
<!-- Need some space in here -->
<button class="btn btn--primary">Submit</button>
</div>
</form>
Uma abordagem seria criar um novo subcomponente, como .stacked-form__footer
, adicionar uma classe adicional a cada botão, .stacked-form__footer-item
e usar seletores descendentes para adicionar alguma margem:
<form class="stacked-form" action="#">
<!-- ... -->
- <div class="stacked-form__section">
+ <div class="stacked-form__section stacked-form__footer">
- <button class="btn btn--secondary">Cancel</button>
- <button class="btn btn--primary">Submit</button>
+ <button class="stacked-form__footer-item btn btn--secondary">Cancel</button>
+ <button class="stacked-form__footer-item btn btn--primary">Submit</button>
</div>
</form>
Precisamos criar um pouco de CSS:
.stacked-form__footer {
text-align: right;
}
.stacked-form__footer-item {
margin-right: 1rem;
&:last-child {
margin-right: 0;
}
}
Mas e se tivéssemos esse mesmo problema em um subnav em algum lugar ou um cabeçalho?
Nós não podemos reutilizar o .stacked-form__footer
fora de um .stacked-form
, então, acabamos criando um novo subcomponente dentro do nosso cabeçalho:
<header class="header-bar">
<h2 class="header-bar__title">New Product</h2>
+ <div class="header-bar__actions">
+ <button class="header-bar__action btn btn--secondary">Cancel</button>
+ <button class="header-bar__action btn btn--primary">Save</button>
+ </div>
</header>
Mas…temos que duplicar o esforço que dedicamos à construção do .stacked-form__footer
no .header-bar__actions
.
Isso parece muito com o problema que encontramos no início, com nomes de classes baseados em conteúdo, não é?
Uma maneira de resolver esse problema é criar um componente totalmente novo que seja mais fácil de reutilizar e use a composição.
Talvez nós façamos algo como .actions-list
:
.actions-list {
text-align: right;
}
.actions-list__item {
margin-right: 1rem;
&:last-child {
margin-right: 0;
}
}
Agora podemos nos livrar completamente de .stacked-form__footer
e .header-bar__actions
, e usar .actions-list
em ambas as situações:
<!-- Stacked form -->
<form class="stacked-form" action="#">
<!-- ... -->
<div class="stacked-form__section">
<div class="actions-list">
<button class="actions-list__item btn btn--secondary">Cancel</button>
<button class="actions-list__item btn btn--primary">Submit</button>
</div>
</div>
</form>
<!-- Header bar -->
<header class="header-bar">
<h2 class="header-bar__title">New Product</h2>
<div class="actions-list">
<button class="actions-list__item btn btn--secondary">Cancel</button>
<button class="actions-list__item btn btn--primary">Save</button>
</div>
</header>
Mas e se uma dessas listas de ações fosse justificada à esquerda e a outra fosse justificada à direita? Nós fazemos Devemos criar os modificadores .actions-list--left
e .actions-list--right
?
Fase 4: Componentes agnósticos de conteúdo + classes utilitárias
Tentar criar esses nomes de componentes o tempo todo é exaustivo.
Quando você faz modificadores como .actions-list--left
, você está criando um modificador de componente totalmente novo apenas para atribuir uma única propriedade CSS. Já tem left
no nome, então você não vai enganar ninguém dizendo que é "semântico".
E se tivéssemos outro componente que precisa de modificadores de alinhamento à esquerda e alinhamento à direita, criaríamos novos modificadores de componente para isso também?
Este é o mesmo problema que enfrentamos quando decidimos remover .stacked-form__footer
e .header-bar__actions
e substituí-los com um único .actions-list
:
Nós preferimos a composição à duplicação.
Então, se tivéssemos duas listas de ações, uma que precisava ser alinhada à esquerda e outra que precisava estar alinhada corretamente, como poderíamos resolver esse problema com a composição?
Utilitários de alinhamento
Para resolver este problema com a composição, precisamos ser capazes de adicionar uma nova classe reutilizável ao nosso componente que nos dê o efeito desejado.
Nós já estávamos indo para chamar nossos modificadores .actions-list--left
e .actions-list--right
, por isso, não há nenhuma razão para não chamar essas novas classes algo como .align-left
e .align-right
:
.align-left {
text-align: left;
}
.align-right {
text-align: right;
}
Agora podemos usar a composição para tornar nossos botões de formulários alinhados à esquerda:
<form class="stacked-form" action="#">
<!-- ... -->
<div class="stacked-form__section">
<div class="actions-list align-left">
<button class="actions-list__item btn btn--secondary">Cancel</button>
<button class="actions-list__item btn btn--primary">Submit</button>
</div>
</div>
</form>
E nossos botões de cabeçalho alinhados à direita:
<header class="header-bar">
<h2 class="header-bar__title">New Product</h2>
<div class="actions-list align-right">
<button class="actions-list__item btn btn--secondary">Cancel</button>
<button class="actions-list__item btn btn--primary">Save</button>
</div>
</header>
Não tenha medo
Ao ver as palavras “esquerda” e “direita” em seu HTML fizer com que você se sinta desconfortável, lembre-se de que estamos usando componentes com nomes de padrões visuais em nossa interface do usuário há séculos.
Não há como fingir que .stacked-form
é mais "semântico" que .align-right
; ambos são nomeados referente a como eles afetam a apresentação da marcação, e estamos usando essas classes em nossa marcação para alcançar um resultado de apresentação específico.
Estamos escrevendo HTML dependente de CSS. Se quisermos mudar nosso formulário de um .stacked-form
para um .horizontal-form
, fazemos isso na marcação, não no CSS.
Excluindo abstrações inúteis
O interessante dessa solução é que nosso componente .actions-list
, agora, é basicamente inútil; Tudo o que fazia antes era alinhar o conteúdo à direita.
Vamos deletar:
- .actions-list {
- text-align: right;
- }
.actions-list__item {
margin-right: 1rem;
&:last-child {
margin-right: 0;
}
}
Mas agora é um pouco estranho ter um .actions-list__item
sem .actions-list
. Existe outra maneira de resolver nosso problema original sem criar um componente .actions-list__item
?
Vamos pensar na razão pela qual criamos este componente. Ele adiciona um pouco de margem entre dois botões, .actions-list
era uma metáfora bastante decente para uma lista de botões porque era genérica e razoavelmente reutilizável. Certamente poderia haver situações em que precisássemos da mesma quantidade de espaçamento entre itens que não são "ações", certo?
Talvez um nome mais reutilizável seria algo parecido com .spaced-horizontal-list
? Nós já deletamos o .actions-list
, porque na verdade, só os componentes filhos é que precisam de qualquer estilo.
Utilitários de espaçamento
Se apenas os componentes filhos precisam de estilo, talvez fosse mais simples modelar eles independentemente, ou usar pseudo-seletores sofisticados para estilizá-los como um grupo?
A maneira mais reutilizável de adicionar algum espaçamento ao lado de um elemento seria uma classe que diz “esse elemento deve ter algum espaço próximo a ele”.
Nós já adicionamos utilitários como .align-left
e .align-right
, e se fizéssemos um novo utilitário apenas para adicionar alguma margem certa?
Vamos criar uma nova classe de utilitário, algo como .mar-r-sm
, para adicionar uma pequena quantidade de margem à direita de um elemento:
- .actions-list__item {
- margin-right: 1rem;
- &:last-child {
- margin-right: 0;
- }
- }
+ .mar-r-sm {
+ margin-right: 1rem;
+ }
Aqui está como nosso formulário e cabeçalho se pareceria:
<!-- Stacked form -->
<form class="stacked-form" action="#">
<!-- ... -->
<div class="stacked-form__section align-left">
<button class="btn btn--secondary mar-r-sm">Cancel</button>
<button class="btn btn--primary">Submit</button>
</div>
</form>
<!-- Header bar -->
<header class="header-bar">
<h2 class="header-bar__title">New Product</h2>
<div class="align-right">
<button class="btn btn--secondary mar-r-sm">Cancel</button>
<button class="btn btn--primary">Save</button>
</div>
</header>
Todo o conceito de um .actions-list
foi removido. Nosso CSS é menor e nossas classes são mais reutilizáveis.
Fase 5: CSS orientado a classes utilitárias
Uma vez que isso clicou para mim, não demorou muito para que eu tivesse construído um conjunto completo de classes utilitárias para ajustes visuais comuns que eu precisava, coisas como:
- Tamanhos, cores e pesos de texto
- Cores, larguras e posições da borda
- Cores de fundo
- Utilitários do Flexbox
padding
emargin
A coisa surpreendente sobre isso é que, você pode criar componentes de interface totalmente novos sem escrever nenhum novo CSS.
Dê uma olhada neste tipo de componente “cartão de produto” de um projeto meu:
Aqui está a aparência da minha marcação:
<div class="card rounded shadow">
<a href="..." class="block">
<img class="block fit" src="...">
</a>
<div class="py-3 px-4 border-b border-dark-soft flex-spaced flex-y-center">
<div class="text-ellipsis mr-4">
<a href="..." class="text-lg text-medium">
Test-Driven Laravel
</a>
</div>
<a href="..." class="link-softer">
@icon('link')
</a>
</div>
<div class="flex text-lg text-dark">
<div class="py-2 px-4 border-r border-dark-soft">
@icon('currency-dollar', 'icon-sm text-dark-softest mr-4')
<span>$3,475</span>
</div>
<div class="py-2 px-4">
@icon('user', 'icon-sm text-dark-softest mr-4')
<span>25</span>
</div>
</div>
</div>
O número de classes usadas aqui pode fazer com que você se atrapalhe no início, mas vamos dizer que nós queremos fazer deste um componente CSS real ao invés de compor classes utilitárias. Como poderíamos chamá-lo?
Não queremos usar nomes específicos de conteúdo, porque o nosso componente só pode ser usado em um contexto.
Talvez algo assim?
.image-card-with-a-full-width-section-and-a-split-section
Claro que não, isso é ridículo. Ao invés disso, queremos compor componentes menores, como falamos antes.
Quais podem ser esses componentes?
Bom, talvez esse seja um .card
?. Nem todos os cartões tem sombra, por isso poderíamos ter um modificador .card--shadow
, ou poderíamos criar um utilitário .shadow
que pudesse ser aplicado a qualquer elemento. Isso soa mais reutilizável, então vamos fazer isso.
Acontece que alguns .card
do nosso site não têm cantos arredondados, mas esse tem. Poderíamos fazer isso com .card--rounded
, mas temos outros elementos no site que às vezes são arredondados da mesma forma, e esses não são cartões. Um utilitário .rounded
seria mais reutilizável.
E a imagem no topo? Talvez seja algo como um .img--fitted
, para preencher o cartão? Bem, há alguns outros pontos no site onde precisamos encaixar algo na largura dos componentes pais e nem sempre é uma imagem. Talvez apenas um utilitário .fit
seria melhor.
E também esse espaço…
Bom, você pode ver onde estou indo com isso!
Se você seguir essa trilha, com foco suficiente na reutilização, construir esse componente a partir de utilitários reutilizáveis é o destino natural.
Consistência reforçada
Um dos maiores benefícios de usar utilitários pequenos e compostos é que cada desenvolvedor que entra em sua equipe, sempre irá usar valores de um conjunto fixo de opções.
Quantas vezes você precisou estilizar algum HTML e pensou: “este texto precisa ser um pouco mais escuro”, então chegou no CSS e, pá! Uma função darken()
para ajustar um $text-color
?
Ou talvez, “essa fonte deve ser um pouco menor” e, sem pensar muito, um font-size: .85em
nasce no componente em que você está trabalhando?
Parece que você está fazendo as coisas “certo”, porque você está usando uma cor relativa ou um tamanho de fonte relativo, não apenas valores arbitrários.
Mas e se você decidir escurecer o texto em 10% para o seu componente, e outra pessoa o escurecer em 12% para outro componente semelhante? Antes que você perceba, você acaba com 402 cores de texto exclusivas na sua folha de estilo.
Isso acontece em todas as bases de código, onde a maneira de estilizar algo é sempre escrever um novo CSS:
- GitLab: 402 cores de texto, 239 cores de fundo, 59 tamanhos de fonte
- Buffer: 124 cores de texto, 86 cores de fundo, 54 tamanhos de fonte
- HelpScout: 198 cores de texto, 133 cores de fundo, 67 tamanhos de letra
- Gumroad: 91 cores de texto, 28 cores de fundo, 48 tamanhos de letra
- Stripe: 189 cores de texto, 90 cores de fundo, 35 tamanhos de letra
- GitHub: 163 cores de texto, 147 cores de fundo, 56 tipos de letra
- ConvertKit: 128 cores de texto, 124 cores de fundo, 70 tamanhos de letra
Isso porque cada novo pedaço de CSS que você escreve é uma tela em branco; não há nada que impeça você de usar qualquer valor que você queira.
Você poderia tentar reforçar a consistência através de variáveis ou mixins, mas cada linha de um novo CSS ainda é uma oportunidade para uma nova complexidade; Adicionar mais CSS nunca tornará seu CSS mais simples.
Se, ao invés disso, a solução para modelar algo for aplicar classes existentes, de repente, esse problema de tela em branco desaparece.
Quer silenciar algum texto escuro? Adicione um .text-dark-soft
.
Precisa tornar o tamanho da fonte um pouco menor? Use a .text-sm
.
Quando todos em um projeto estão escolhendo seus estilos de um conjunto curado de opções limitadas, seu CSS deixa de crescer linearmente com o tamanho do seu projeto e você obtém consistência de graça.
Você ainda deve criar componentes
Uma das áreas em que a minha opinião difere um pouco de alguns dos defensores do CSS Funcional, é que eu não acho que você deva construir coisas apenas com utilitários.
Se você olhar para alguns dos populares frameworks baseados em utilitários como o Tachyons (que é um projeto fantástico), verá que eles criam até mesmo estilos de botões a partir de utilitários puros:
<button class="f6 br3 ph3 pv2 white bg-purple hover-bg-light-purple">
Button Text
</button>
f6
: Use o sexto tamanho de fonte na escala de tamanho de fonte (.875rem em Tachyons)br3
: Use o terceiro raio de borda na escala do raio de borda (.5rem)ph3
: Use o terceiro tamanho na escala de preenchimento para preenchimento horizontal (1rem)pv2
: Use o segundo tamanho na escala de preenchimento para preenchimento vertical (.5rem)white
: Use texto brancobg-purple
: Use um fundo roxohover-bg-light-purple
: Use um fundo roxo claro ao passar o mouse
Se você precisar de vários botões com essa mesma combinação de classes, a abordagem recomendada com o Tachyons é criar uma abstração por meio de templates, e não por CSS.
Se você estivesse usando o Vue.js por exemplo, poderia criar um componente que usaria assim:
<ui-button color="purple">Save</ui-button>
Implementando da seguinte forma:
<template>
<button class="f6 br3 ph3 pv2" :class="colorClasses">
<slot></slot>
</button>
</template>
<script>
export default {
props: ['color'],
computed: {
colorClasses() {
return {
purple: 'white bg-purple hover-bg-light-purple',
lightGray: 'mid-gray bg-light-gray hover-bg-light-silver',
// ...
}[this.color]
}
}
}
</script>
Essa é uma ótima abordagem para muitos projetos, mas ainda acho que há muitos casos de uso em que é mais prático criar um componente CSS do que criar um componente baseado em template.
Para o tipo de projetos em que trabalho, geralmente é mais simples criar uma nova classe .btn-purple
que agrupe esses 7 utilitários do que comprometer a criar um template pequeno para cada widget no
Crie suas classes utilitárias primeiro
A razão de eu chamar essaa abordagem de CSS orientado a classes utilitárias (Utility-first CSS) é porque eu tento construir tudo o que puder/baseado no design, em utilitários e extraio padrões repetidos à medida que surgem.
Se você estiver usando Less como seu pré-processador, você pode usar classes existentes como mixins. Isso significa que, se eu criar este .btn-purple
, leva apenas um pouco de magia com vários cursores em seu editor:
Infelizmente, você não pode fazer isso no Sass ou Stylus sem criar um mixin separado para cada classe de utilitários, então é um pouco mais de trabalho por lá.
Nem sempre é possível que cada declaração em um componente venha de um utilitário, é claro. Interações complexas entre elementos como mudar a propriedade de um elemento filho quando passar o mouse sobre o elemento pai, são difíceis de fazer apenas com utilitários, então use seu melhor julgamento e faça o que parecer mais simples.
Não há mais abstração prematura
Adotar uma abordagem de criar componentes primeiro, em CSS, significa que você cria componentes para as coisas, mesmo que elas nunca sejam reutilizadas. Essa abstração prematura é a fonte de muito inchaço e complexidade nas folhas de estilo.
Pegue uma barra de navegação, por exemplo. Quantas vezes no seu aplicativo você reescreve a marcação para o seu navegador principal?
Em meus projetos, normalmente só faço isso uma vez; no meu arquivo de layout principal.
Se você construir coisas com utilitários primeiro e apenas extrair componentes quando observar uma duplicação preocupante, provavelmente nunca precisará extrair um componente “barra de navegação”.
Ao invés disso, sua barra de navegação pode ser algo como isto:
<nav class="bg-brand py-4 flex-spaced">
<div><!-- Logo goes here --></div>
<div>
<!-- Menu items go here -->
</div>
</nav>
Não há nada que valha a pena extrair.
Isso não é apenas estilos inline?
É fácil olhar para essa abordagem e pensar que é como jogar tags de estilo em seus elementos HTML e adicionar as propriedades que você precisa, mas na minha experiência é muito diferente.
Com estilos inline, não há restrições sobre quais valores você escolhe.
Uma tag poderia ter font-size: 14px
, outra poderia ter font-size: 13px
, outra poderia ter font-size: .9em
e outra poderia ter font-size: .85rem
.
É o mesmo problema da “tela em branco” que você enfrenta ao escrever um novo CSS para cada novo componente.
Utilitários forçam você a escolher:
É isso text-sm
ou text-xs
?
Devo usar py-3
ou py-4
?
Eu quero text-dark-soft
ou text-dark-faint
?
Você não pode simplesmente escolher qualquer valor desejado; você tem que escolher de uma lista selecionada.
Em vez de 380 cores de texto, você acaba com 10 ou 12.
Minha experiência é que a criação de “utilitários em primeiro lugar” leva a designs e aparências mais consistentes do que “componentes em primeiro lugar”, o que não é intuitivo, como pode parecer à primeira vista.
Onde começar
Se esta abordagem parece interessante para você, aqui estão alguns frameworks que vale a pena conferir:
Recentemente, também lancei minha própria estrutura com PostCSS de código aberto, chamada Tailwind CSS. Foi projetada em torno dessa ideia de trabalhar primeiro com classes utilitárias e extrair componentes de padrões repetidos:
Se você estiver interessado em dar uma olhada, vá até o site Tailwind CSS e experimente.
⭐️ Créditos
- CSS Utility Classes and “Separation of Concerns”, escrito originalmente por Adam Wathan