Creación de una arquitectura CSS escalable con BEM y clases de utilidad
- CSS Globals en 30 segundos
- BEM en 30 segundos
- Clases de utilidad en 30 segundos.
- Un ejemplo de la vida real.
- CSS Globals y clases de utilidad al rescate
- Finalmente, ¡introduzcamos las clases de utilidad a la mezcla!
- ¿Por qué no utilizamos simplemente clases de utilidad?
- Resultado final
- Estructura de archivos
- Conclusión
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 intentamos cumplir los dos objetivos siguientes:
- Eficiencia : queremos reducir el tiempo dedicado a pensar en cómo se deben hacer las cosas y aumentar el tiempo de hacer las cosas.
- Coherencia : queremos asegurarnos de que todos los desarrolladores estén en la misma página.
Durante el último año y medio, estuvo trabajando en una biblioteca de componentes y un marco de interfaz de usuario llamado CodyFrame. Actualmente tenemos más de 220 componentes. Estos componentes no son módulos aislados: son patrones reutilizables, a menudo fusionados 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 global, 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 espacio, escala de tipografía, colores, etc.). Los globales usan tokens para mantener el diseño consistente en todos los componentes y reducir el tamaño de su CSS.
A continuación se muestra 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.
He aquí un ejemplo:
header a href="#0"!-- ... --/a nav ul lia href="#0"Homepage/a/li lia href="#0"About/a/li lia href="#0"Contact/a/li /ul /nav/header
- Un bloque es un componente reutilizable.
- Un elemento es hijo del bloque (por ejemplo,
.block__element
) - Un modificador es una variación de un bloque/elemento (por ejemplo,
.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 h1Title/h1 pLorem 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, puedes construir componentes completos a partir de clases de utilidad:
article h1Title/h1 pLorem ipsum dolor sit amet consectetur adipisicing elit./p/article
Puede conectar clases de utilidad a CSS global:
/* 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 utilizando 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 utilizando únicamente el enfoque BEM y señalaremos los problemas que pueden enfrentar al utilizar únicamente BEM. A continuación, veremos cómo los Globales reducen 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:
Comenzamos este experimento creando la galería usando solo BEM:
div article a href="#0" figure img src="/image.jpg" alt="Image description" /figure
div h1spanTitle of the card/span/h1
pLorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?/p /div
div aria-hidden="true" svg viewBox="0 0 24 24"!-- icon --/svg /div /a /article
article!-- card --/article article!-- card --/article article!-- card --/article/div
En este ejemplo, tenemos dos componentes: .grid
y .card
. El primero se utiliza 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 utilizar 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 utiliza BEM (o un método de denominación similar), terminará 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 */
Lidiar con la herencia y la especificidad en grandes proyectos es doloroso. ¡Esa sensación cuando tu CSS no parece funcionar y descubres que otra clase lo ha sobrescrito! BEM, por otro lado, crea algún tipo de margen para sus componentes y mantiene baja la especificidad.
Pero… hay dos desventajas principales de usar solo BEM:
- Nombrar demasiadas cosas es frustrante
- 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 encontrar tantos nombres significativos (y que todos tus compañeros de equipo utilizan 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 h1spanTitle of the card/span/h1 pLorem ipsum dolor.../p pLorem ipsum dolor.../p !-- --/div
¿Como lo llamas? Podrías considerarlo una variación del .card__description
elemento y optar por .card__description .card__description--small
. O bien, podrías crear un nuevo elemento, algo como .card__small, .card__small-p
o .card__tag
. ¿Ves adónde voy? Nadie quiere perder tiempo pensando en nombres de clases. BEM es genial siempre y cuando no tengas que nombrar demasiadas cosas .
El segundo problema tiene que ver con personalizaciones menores. Por ejemplo, imagina que tienes que crear una variación del componente de la tarjeta donde el texto está alineado en el centro.
Probablemente harás algo como esto:
div !-- -- h1spanTitle of the card/span/h1 pLorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?/p/div
style .card__content--center { text-align: center; }/style
Uno de sus compañeros de equipo, que trabaja en otro componente ( .banner
), enfrenta el mismo problema. También crean una variación para su componente:
div/div
style .banner--text-center { text-align: center; }/style
Ahora imagina que tienes 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 del banner, instintivamente puedes escribir algo así banner banner--center
en tu HTML, porque siempre usas --center
cuando creas variaciones donde el texto está alineado al centro. ¡It doesn't work! Su única opción es abrir el archivo CSS del componente del banner, inspeccionar el código y averiguar qué clase se debe aplicar para alinear el texto en el centro.
¿Cuánto tiempo tardaría, 5 minutos? Multiplica 5 minutos por todas las veces que esto te sucede en un día, a ti ya todos tus compañeros, y te das cuenta de cuánto tiempo se pierde. Además, agrega nuevas clases que hagan lo mismo contribuyen a inflar tu CSS.
CSS Globals y clases de utilidad al rescate
La primera ventaja de configurar 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 utilizamos unidades "em" para todos los márgenes y rellenos, todo el sistema de espaciado se actualiza de inmediato generando un efecto en cascada.
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 utilizar los globales para almacenar componentes de comportamiento que se pueden combinar con todos los demás componentes. Por ejemplo, en CodyFrame, definimos una .text-component
clase que se utiliza como "contenedor de texto". Se encarga de la altura de la línea, el espaciado vertical, el estilo básico y otras cosas.
Si volvemos a nuestro ejemplo de tarjeta, el .card__content
elemento podría reemplazarse por lo siguiente:
!-- without globals --div h1spanTitle of the card/span/h1 pLorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?/p/div
!-- with globals --div h1spanTitle of the card/span/h1 pLorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?/p/div
El componente de texto se encargará del formato del texto y lo hará coherente 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 particularmente útiles si desea poder personalizar el componente más adelante sin tener que verificar su CSS.
Así es como cambia la estructura del componente de la tarjeta si intercambiamos algunas clases BEM con clases de utilidad:
article a href="#0" figure img src="image.jpg" alt="Image description" /figure
div h1spanTitle of the card/span/h1 pLorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?/p /div
div aria-hidden="true" svg 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 tendrás que ocuparte mucho de nombrar cosas. Dicho esto, no podemos evitar por completo el problema de los nombres: incluso si crea componentes Vue/React/SomeOtherFramework a partir de clases de utilidad, aún debe 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 la tarjeta con un título más grande? Reemplace texto-lg con texto-xl. ¿Qué pasa si quieres cambiar el color del icono? Reemplace el color blanco con el color primario. ¿Qué tal alinear el texto en el centro? Agregue centro de texto al elemento del componente de texto. ¡Menos tiempo pensando, más tiempo haciendo!
¿Por qué no utilizamos 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 utilizamos sólo clases de utilidad? Dos razones principales:
Al utilizar BEM junto con clases de utilidad, el HTML es más fácil de leer y personalizar.
Utilice BEM para:
- Secar el HTML del CSS que no planea personalizar (por ejemplo, transiciones de comportamiento similares a CSS, posicionamiento, efectos de desplazamiento/enfoque),
- animaciones/efectos avanzados.
Utilice clases de utilidad para:
- las propiedades "frecuentemente personalizadas", que a menudo se utilizan para crear variaciones de componentes (como relleno, margen, alineación de texto, etc.),
- elementos que son difíciles de identificar con un nombre de clase nuevo y significativo (por ejemplo, necesita un elemento principal con
position: relative
→ creardivdiv/div/div
).
Ejemplo:
!-- use only Utility classes --article !-- card content --/article
!-- use BEM + Utility classes --article !-- card content --/article
Por estos motivos, le sugerimos que no agregue la regla !important a sus clases de servicios públicos. Usar clases de utilidad no tiene por qué ser como usar un martillo. ¿Crees que sería beneficioso acceder y modificar una propiedad CSS en HTML? Utilice una clase de utilidad. ¿Necesita un montón de reglas que no necesiten edición? Escríbelos en tu CSS. No es necesario que este proceso sea perfecto la primera vez que lo realiza: puede modificar el componente más adelante si es necesario. Puede parecer 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 necesitas abrir tu archivo CSS.
Consideremos, 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 utilidades de cuadrícula para personalizar el diseño.
Estructura de archivos
Así es como se vería la estructura de un proyecto creado utilizando 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 PostCSS para compilar cada /component/componentName.css
archivo nuevo style.css
). Siéntete libre de organizar los globales como prefieras; También puedes crear un solo globals.css
archivo y evitar separar los globales en diferentes archivos.
Conclusión
Trabajar en proyectos de gran escala requiere una arquitectura sólida si quieres abrir tus archivos meses después y no perderte. 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 transversales (globales), el uso de 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!
Deja un comentario