Creación de una arquitectura CSS escalable con BEM y clases de utilidad | Programar Plus

Mantener un proyecto CSS a gran escala es difícil. A lo largo de los años, hemos sido testigos de diferentes enfoques destinados a facilitar el proceso de escritura de CSS escalable. Al final, todos tratamos de cumplir los siguientes dos objetivos:

  1. Eficiencia: queremos reducir el tiempo dedicado a pensar en cómo se deben hacer las cosas y aumentar el tiempo haciendo las cosas.
  2. Consistencia: queremos asegurarnos de que todos los desarrolladores estén en la misma página.

Durante el último año y medio, he estado trabajando en una biblioteca de componentes y un marco front-end llamado CodyFrame. Actualmente tenemos más de 220 componentes. Estos componentes no son módulos aislados: son patrones reutilizables, a menudo combinados entre sí para crear plantillas complejas.

Los desafíos de este proyecto han obligado a nuestro equipo a desarrollar una forma de construir arquitecturas CSS escalables. Este método se basa en CSS globales, BEM y clases de utilidad.

¡Estoy feliz de compartirlo! 👇

CSS Globals en 30 segundos

Los globales son archivos CSS que contienen reglas que se aplican transversalmente a todos los componentes (por ejemplo, escala de espaciado, escala de tipografía, colores, etc.). Globals usa tokens para mantener el diseño consistente en todos los componentes y reducir el tamaño de su CSS.

Aquí hay un ejemplo de reglas globales de tipografía:

/* Typography | Global */
:root {
  /* body font size */
  --text-base-size: 1em;


  /* type scale */
  --text-scale-ratio: 1.2;
  --text-xs: calc((--text-base-size / var(--text-scale-ratio)) / var(--text-scale-ratio));
  --text-sm: calc(var(--text-xs) * var(--text-scale-ratio));
  --text-md: calc(var(--text-sm) * var(--text-scale-ratio) * var(--text-scale-ratio));
  --text-lg: calc(var(--text-md) * var(--text-scale-ratio));
  --text-xl: calc(var(--text-lg) * var(--text-scale-ratio));
  --text-xxl: calc(var(--text-xl) * var(--text-scale-ratio));
}


@media (min-width: 64rem) { /* responsive decision applied to all text elements */
  :root {
    --text-base-size: 1.25em;
    --text-scale-ratio: 1.25;
  }
}


h1, .text-xxl   { font-size: var(--text-xxl, 2.074em); }
h2, .text-xl    { font-size: var(--text-xl, 1.728em); }
h3, .text-lg    { font-size: var(--text-lg, 1.44em); }
h4, .text-md    { font-size: var(--text-md, 1.2em); }
.text-base      { font-size: --text-base-size; }
small, .text-sm { font-size: var(--text-sm, 0.833em); }
.text-xs        { font-size: var(--text-xs, 0.694em); }

BEM en 30 segundos

BEM (Bloques, Elementos, Modificadores) es una metodología de nomenclatura destinada a crear componentes reutilizables.

Aquí hay un ejemplo:

<header class="header">
  <a href="https://css-tricks.com/building-a-scalable-css-architecture-with-bem-and-utility-classes/#0" class="header__logo"><!-- ... --></a>
  <nav class="header__nav">
    <ul>
      <li><a href="https://css-tricks.com/building-a-scalable-css-architecture-with-bem-and-utility-classes/#0" class="header__link header__link--active">Homepage</a></li>
      <li><a href="https://css-tricks.com/building-a-scalable-css-architecture-with-bem-and-utility-classes/#0" class="header__link">About</a></li>
      <li><a href="https://css-tricks.com/building-a-scalable-css-architecture-with-bem-and-utility-classes/#0" class="header__link">Contact</a></li>
    </ul>
  </nav>
</header>
  • A cuadra es un componente reutilizable
  • Un elemento es un hijo del bloque (por ejemplo, .block__element)
  • A modificador es una variación de un bloque/elemento (p. ej., .block--modifier, .block__element--modifier).

Clases de utilidad en 30 segundos

Una clase de utilidad es una clase CSS destinada a hacer una sola cosa. Por ejemplo:

<section class="padding-md">
  <h1>Title</h1>
  <p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
</section>


<style>
  .padding-sm { padding: 0.75em; }
  .padding-md { padding: 1.25em; }
  .padding-lg { padding: 2em; }
</style>

Potencialmente, puede construir componentes completos a partir de clases de utilidad:

<article class="padding-md bg radius-md shadow-md">
  <h1 class="text-lg color-contrast-higher">Title</h1>
  <p class="text-sm color-contrast-medium">Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
</article>

Puede conectar clases de utilidad a CSS globales:

/* Spacing | Global */
:root {
  --space-unit: 1em;
  --space-xs:   calc(0.5 * var(--space-unit));
  --space-sm:   calc(0.75 * var(--space-unit));
  --space-md:   calc(1.25 * var(--space-unit));
  --space-lg:   calc(2 * var(--space-unit));
  --space-xl:   calc(3.25 * var(--space-unit));
}

/* responsive rule affecting all spacing variables */
@media (min-width: 64rem) {
  :root {
    --space-unit:  1.25em; /* 👇 this responsive decision affects all margins and paddings */
  }
}

/* margin and padding util classes - apply spacing variables */
.margin-xs { margin: var(--space-xs); }
.margin-sm { margin: var(--space-sm); }
.margin-md { margin: var(--space-md); }
.margin-lg { margin: var(--space-lg); }
.margin-xl { margin: var(--space-xl); }

.padding-xs { padding: var(--space-xs); }
.padding-sm { padding: var(--space-sm); }
.padding-md { padding: var(--space-md); }
.padding-lg { padding: var(--space-lg); }
.padding-xl { padding: var(--space-xl); }

Un ejemplo de la vida real

Explicar una metodología usando ejemplos básicos no saca a relucir los problemas reales ni las ventajas del método en sí.

¡Construyamos algo juntos!

Crearemos una galería de elementos de tarjetas. Primero, lo haremos usando solo el enfoque BEM, y le indicaremos los problemas que puede enfrentar al usar solo BEM. A continuación, veremos cómo Globals reduce el tamaño de su CSS. Finalmente, haremos que el componente sea personalizable introduciendo clases de utilidad a la mezcla.

He aquí un vistazo al resultado final:

Comencemos este experimento creando la galería usando solo BEM:

<div class="grid">
  <article class="card">
    <a class="card__link" href="https://css-tricks.com/building-a-scalable-css-architecture-with-bem-and-utility-classes/#0">
      <figure>
        <img class="card__img" src="https://css-tricks.com/image.jpg" alt="Image description">
      </figure>


      <div class="card__content">
        <h1 class="card__title-wrapper"><span class="card__title">Title of the card</span></h1>


        <p class="card__description">Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p>
      </div>


      <div class="card__icon-wrapper" aria-hidden="true">
        <svg class="card__icon" viewBox="0 0 24 24"><!-- icon --></svg>
      </div>
    </a>
  </article>


  <article class="card"><!-- card --></article>
  <article class="card"><!-- card --></article>
  <article class="card"><!-- card --></article>
</div>

En este ejemplo, tenemos dos componentes: .grid y .card. El primero se usa para crear el diseño de la galería. El segundo es el componente de la tarjeta.

En primer lugar, permítanme señalar las principales ventajas de usar BEM: baja especificidad y alcance.

/* without BEM */
.grid {}
.card {}
.card > a {}
.card img {}
.card-content {}
.card .title {}
.card .description {}


/* with BEM */
.grid {}
.card {}
.card__link {}
.card__img {}
.card__content {}
.card__title {}
.card__description {}

Si no usa BEM (o un método de nomenclatura similar), termina creando relaciones de herencia (.card > a).

/* without BEM */
.card > a.active {} /* high specificity */


/* without BEM, when things go really bad */
div.container main .card.is-featured > a.active {} /* good luck with that 😦 */


/* with BEM */
.card__link--active {} /* low specificity */

Tratar con la herencia y la especificidad en grandes proyectos es doloroso. ¡Esa sensación cuando tu CSS parece no funcionar y descubres que otra clase lo ha sobrescrito 😡! BEM, por otro lado, crea algún tipo de alcance para sus componentes y mantiene baja la especificidad.

Pero… Hay dos desventajas principales de usar solo BEM:

  1. Nombrar demasiadas cosas es frustrante
  2. Las personalizaciones menores no son fáciles de hacer o mantener

En nuestro ejemplo, para estilizar los componentes, hemos creado las siguientes clases:

.grid {}
.card {}
.card__link {}
.card__img {}
.card__content {}
.card__title-wrapper {}
.card__title {}
.card__description {}
.card__icon-wrapper {}
.card__icon {}

El número de clases no es el problema. El problema es tener tantos nombres significativos (y que todos tus compañeros de equipo usen el mismo criterio de nomenclatura).

Por ejemplo, imagina que tienes que modificar el componente de la tarjeta incluyendo un párrafo adicional más pequeño:

<div class="card__content">
  <h1 class="card__title-wrapper"><span class="card__title">Title of the card</span></h1>
  <p class="card__description">Lorem ipsum dolor...</p>
  <p class="card__description card__description--small">Lorem ipsum dolor...</p> <!-- 👈 -->
</div>

¿Como lo llamas? Podrías considerarlo una variación del .card__description elemento e ir a por .card__description .card__description--small. O bien, podría crear un nuevo elemento, algo como .card__small, .card__small-p, o .card__tag. ¿Ves a dónde voy? Nadie quiere perder el tiempo pensando en los nombres de las clases. BEM es genial siempre y cuando no tengas que nombrar demasiadas cosas.

El segundo problema es lidiar con personalizaciones menores. Por ejemplo, imagina que tienes que crear una variación del componente de la tarjeta donde el texto está alineado al centro.

Probablemente harás algo como esto:

<div class="card__content card__content--center"> <!-- 👈 -->
  <h1 class="card__title-wrapper"><span class="card__title">Title of the card</span></h1>
  <p class="card__description">Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p>
</div>


<style>
  .card__content--center { text-align: center; }
</style>

Uno de tus compañeros de equipo, trabajando en otro componente (.banner), se enfrenta al mismo problema. También crean una variación para su componente:

<div class="banner banner--text-center"></div>


<style>
  .banner--text-center { text-align: center; }
</style>

Ahora imagine que tiene que incluir el componente de banner en una página. Necesita la variación donde el texto está alineado en el centro. Sin verificar el CSS del componente de banner, puede escribir instintivamente algo como banner banner--center en tu HTML, porque siempre usas --center cuando crea variaciones donde el texto está alineado al centro. ¡No funciona! Su única opción es abrir el archivo CSS del componente de banner, inspeccionar el código y averiguar qué clase se debe aplicar para alinear el texto en el centro.

¿Cuánto tiempo tomaría, 5 minutos? Multiplica 5 minutos por todas las veces que esto te pasa en un día, a ti y a todos tus compañeros, y te das cuenta de cuánto tiempo se pierde. Además, agregar nuevas clases que hacen lo mismo contribuye a inflar tu CSS.

CSS Globals y clases de utilidad al rescate

La primera ventaja de establecer estilos globales es tener un conjunto de reglas CSS que se aplican a todos los componentes.

Por ejemplo, si establecemos reglas de respuesta en los globales de espaciado y tipografía, estas reglas también afectarán a los componentes de la cuadrícula y la tarjeta. En CodyFrame, aumentamos el tamaño de fuente del cuerpo en un punto de interrupción específico; debido a que usamos unidades “em” para todos los márgenes y rellenos, todo el sistema de espaciado se actualiza a la vez generando un efecto de cascada.

Reglas de respuesta de espaciado y tipografía: sin consultas de medios a nivel de componente

Como consecuencia, en la mayoría de los casos, no necesitará utilizar consultas de medios para aumentar el tamaño de fuente o los valores de márgenes y rellenos.

/* without globals */
.card { padding: 1em; }


@media (min-width: 48rem) {
  .card { padding: 2em; }
  .card__content { font-size: 1.25em; }
}


/* with globals (responsive rules intrinsically applied) */
.card { padding: var(--space-md); }

¡No solo eso! Puede usar los globales para almacenar componentes de comportamiento que se pueden combinar con todos los demás componentes. Por ejemplo, en CodyFrame, definimos un .text-component clase que se utiliza como un “envoltorio de texto”. Se ocupa de la altura de la línea, el espacio vertical, el estilo básico y otras cosas.

Si volvemos a nuestro ejemplo de tarjeta, el .card__content elemento podría ser reemplazado por lo siguiente:

<!-- without globals -->
<div class="card__content">
  <h1 class="card__title-wrapper"><span class="card__title">Title of the card</span></h1>
  <p class="card__description">Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p>
</div>


<!-- with globals -->
<div class="text-component">
  <h1 class="text-lg"><span class="card__title">Title of the card</span></h1>
  <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p>
</div>

El componente de texto se encargará del formato del texto y lo hará consistente en todos los bloques de texto de su proyecto. Además, ya hemos eliminado un par de clases BEM.

¡Finalmente, introduzcamos las clases de utilidad a la mezcla!

Las clases de utilidad son especialmente útiles si desea poder personalizar el componente más adelante sin tener que comprobar su CSS.

Así es como cambia la estructura del componente de la tarjeta si intercambiamos algunas clases BEM con clases de utilidad:

<article class="card radius-lg">
  <a href="https://css-tricks.com/building-a-scalable-css-architecture-with-bem-and-utility-classes/#0" class="block color-inherit text-decoration-none">
    <figure>
      <img class="block width-100%" src="https://css-tricks.com/building-a-scalable-css-architecture-with-bem-and-utility-classes/image.jpg" alt="Image description">
    </figure>


    <div class="text-component padding-md">
      <h1 class="text-lg"><span class="card__title">Title of the card</span></h1>
      <p class="color-contrast-medium">Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p>
    </div>


    <div class="card__icon-wrapper" aria-hidden="true">
      <svg class="icon icon--sm color-white" viewBox="0 0 24 24"><!-- icon --></svg>
    </div>
  </a>
</article>

El número de clases BEM (componentes) se ha reducido de 9 a 3:

.card {}
.card__title {}
.card__icon-wrapper {}

Eso significa que no se ocupará mucho de nombrar las cosas. Dicho esto, no podemos evitar el problema de los nombres por completo: incluso si crea componentes Vue/React/SomeOtherFramework a partir de clases de utilidad, todavía tiene que nombrar los componentes.

Todas las demás clases BEM han sido reemplazadas por clases de utilidad. ¿Qué pasa si tienes que hacer una variación de tarjeta con un título más grande? Reemplace texto-lg con texto-xl. ¿Qué sucede si desea cambiar el color del icono? Reemplace el color blanco con el color primario. ¿Qué hay de alinear el texto en el centro? Agregue text-center al elemento de componente de texto. ¡Menos tiempo pensando, más tiempo haciendo!

¿Por qué no usamos simplemente clases de utilidad?

Las clases de utilidad aceleran el proceso de diseño y facilitan la personalización de las cosas. Entonces, ¿por qué no nos olvidamos de BEM y usamos solo clases de utilidad? Dos razones principales:

Al usar BEM junto con clases de utilidad, el HTML es más fácil de leer y personalizar.

Utilice BEM para:

  • DRY-ing el HTML del CSS que no planea personalizar (por ejemplo, transiciones similares a CSS de comportamiento, posicionamiento, efectos de desplazamiento/enfoque),
  • animaciones/efectos avanzados.

Use clases de utilidad para:

  • las propiedades “personalizadas con frecuencia”, a menudo utilizadas para crear variaciones de componentes (como relleno, margen, alineación de texto, etc.),
  • elementos que son difíciles de identificar con un nuevo nombre de clase significativo (por ejemplo, necesita un elemento principal con un position: relative → crear <div class="position-relative"><div class="my-component"></div></div>).

Ejemplo:

<!-- use only Utility classes -->
<article class="position-relative overflow-hidden bg radius-lg transition-all duration-300 hover:shadow-md [email protected] [email protected]">
  <!-- card content -->
</article>


<!-- use BEM + Utility classes -->
<article class="card radius-lg [email protected] [email protected]">
  <!-- card content -->
</article>

Por estas razones, le sugerimos que no agregue la regla !importante a sus clases de utilidad. El uso de clases de utilidad no tiene por qué ser como usar un martillo. ¿Crees que sería beneficioso acceder y modificar una propiedad CSS en el HTML? Utilice una clase de utilidad. ¿Necesita un montón de reglas que no necesitarán edición? Escríbalos en su CSS. No es necesario que este proceso sea perfecto la primera vez que lo haga: puede modificar el componente más adelante si es necesario. Puede sonar laborioso “tener que decidir”, pero es bastante sencillo cuando lo pones en práctica.

Las clases de utilidad no son tu mejor aliado cuando se trata de crear efectos/animaciones únicos.

Piense en trabajar con pseudoelementos o crear efectos de movimiento únicos que requieran curvas Bézier personalizadas. Para esos, aún necesita abrir su archivo CSS.

Considere, por ejemplo, el efecto de fondo animado de la tarjeta que hemos diseñado. ¿Qué tan difícil sería crear tal efecto usando clases de utilidad?

Lo mismo ocurre con la animación de iconos, que requiere fotogramas clave de animación para funcionar:

.card:hover .card__title {
  background-size: 100% 100%;
}


.card:hover .card__icon-wrapper .icon {
  animation: card-icon-animation .3s;
}


.card__title {
  background-image: linear-gradient(transparent 50%, alpha(var(--color-primary), 0.2) 50%);
  background-repeat: no-repeat;
  background-position: left center;
  background-size: 0% 100%;
  transition: background .3s;
}


.card__icon-wrapper {
  position: absolute;
  top: 0;
  right: 0;
  width: 3em;
  height: 3em;
  background-color: alpha(var(--color-black), 0.85);
  border-bottom-left-radius: var(--radius-lg);
  display: flex;
  justify-content: center;
  align-items: center;
}


@keyframes card-icon-animation {
  0%, 100% {
    opacity: 1;
    transform: translateX(0%);
  }
  50% {
    opacity: 0;
    transform: translateX(100%);
  }
  51% {
    opacity: 0;
    transform: translateX(-100%);
  }
}

Resultado final

Aquí está la versión final de la galería de cartas. También incluye clases de utilidad de cuadrícula para personalizar el diseño.

Estructura de archivos

Así es como se vería la estructura de un proyecto creado con el método descrito en este artículo:

project/
└── main/
    ├── assets/
    │   ├── css/
    │   │   ├── components/
    │   │   │   ├── _card.scss
    │   │   │   ├── _footer.scss
    │   │   │   └── _header.scss
    │   │   ├── globals/
    │   │   │   ├── _accessibility.scss
    │   │   │   ├── _breakpoints.scss
    │   │   │   ├── _buttons.scss
    │   │   │   ├── _colors.scss
    │   │   │   ├── _forms.scss
    │   │   │   ├── _grid-layout.scss
    │   │   │   ├── _icons.scss
    │   │   │   ├── _reset.scss
    │   │   │   ├── _spacing.scss
    │   │   │   ├── _typography.scss
    │   │   │   ├── _util.scss
    │   │   │   ├── _visibility.scss
    │   │   │   └── _z-index.scss
    │   │   ├── _globals.scss
    │   │   ├── style.css
    │   │   └── style.scss
    │   └── js/
    │       ├── components/
    │       │   └── _header.js
    │       └── util.js
    └── index.html

Puede almacenar el CSS (o SCSS) de cada componente en un archivo separado (y, opcionalmente, usar complementos de PostCSS para compilar cada nuevo /component/componentName.css archivo en style.css). Siéntete libre de organizar los globales como prefieras; también puedes crear uno solo globals.css archivo y evite separar los globales en diferentes archivos.

Conclusión

Trabajar en proyectos a gran escala requiere una arquitectura sólida si desea abrir sus archivos meses después y no perderse. Existen muchos métodos que abordan este problema (CSS-in-JS, utilidad primero, diseño atómico, etc.).

El método que he compartido con ustedes hoy se basa en la creación de reglas cruzadas (globales), utilizando clases de utilidad para un desarrollo rápido y BEM para clases modulares (de comportamiento).

Puede obtener más información sobre este método en CodyHouse. ¡Cualquier comentario es bienvenido!