Las mejores prácticas para CSS con prioridad por la utilidad

Esta guía es una lista de lecciones útiles aprendidas mientras trabajaba y probaba CSS con utility-first. Su objetivo es ayudarte a utilizar CSS con prioridad a las utilidades de forma eficaz, eliminando los problemas que se mencionan a continuación.

Cómo alinear y ordenar selectores

Ordena tus selectores desde el más pequeño media query/pantalla/contenedor, al más grande.

<!-- Ancho mínimo -->
<button class="font-size:14px sm:font-size:24px lg:font-size:32px"></button>
<!-- Anchura máxima -->
<button class="maxw640px:margin-top:24px tomd:flex-direction:column"></button>

Si el atributo class es largo y tienes múltiples media queries dentro del atributo, pon cada media query en una línea separada. De esta forma puedes ver fácilmente, qué línea es responsable de cada media/container query y qué ha cambiado en el pull request.

<div class="
	width:240px height:240px margin-bottom:24px
	md:margin-bottom:32px md:width:320px
	xl:width:100%
"></div>

Si hay varios atributos en un elemento y la clase tiene muchas utilidades, pon el atributo class al final y divídelo en varias líneas. Esto mantendrá el elemento más legible:

<div id="" aria-label="" data-my-custom-data class="
	width:240px height:240px margin-bottom:24px
	md:margin-bottom:32px md:width:320px
	xl:width:100%
"></div>

Cuando se trata de selectores que están “relacionados” entre sí como font-size/line-height y width/height, ponlos uno al lado del otro. De esta forma es más fácil encontrar los selectores relacionados.

<button class="font-size:18px line-height:24px width:100% height:24px"></button>
<button class="
	display:flex flex-direction:column align-items:center
	font-size:18px line-height:24px
	md:flex-direction:row md:font-size:20px
"></button>

Para reducir el número de clases, también puedes utilizar atajos y funciones CSS modernas para evitar clases innecesarias.

<div class="margin-top:12px margin-right:24px font-size:16px md:font-size:24px xl:font-size:32px"></div>
<div class="margin:12px_24px_0_0 font-size:clamp(16px,10vh,32px)"></div>

Limpiando plantillas hinchadas y atributos de clase duplicados

Debido a la simplicidad del uso de utilidades, es fácil olvidarse de dividir el código en partes reutilizables. Desafortunadamente, no podemos evitar esto, si no queremos tener plantillas ilegibles:

  • Podemos dar estilo a los elementos de forma global, para lo cual podemos usar global custom selectors, o components.
  • Cuando usamos frameworks como React, Vue, Svelte o Angular, podemos crear fácilmente componentes basados en frameworks con slots.
  • También podemos definir componentes dentro de un archivo de configuración o dentro de un archivo (plantilla) donde se utilizan.

Cada uno de estos enfoques tiene su propósito que se explica a continuación.

Creación de componentes reutilizables

En caso de que trabajes con frameworks como Nuxt, Next, Vue, React, Svelte o Lit, puedes definir componentes y reutilizarlos. De esta manera, usted tiene estilos para tales componentes sólo en un lugar y no bloat su HTML en otros lugares.

Además, puedes restringir la cantidad de posibles variantes de estilo del componente al renderizarlo basándote en las props pasadas.

// MyButton.vue
<template>
	<button class="color:red font-size:24px">
		<slot></slot>
	</button>
</template>

// Page.vue
<template>
	<MyButton>¡Hazme clic!</MyButton>
</template>

Un enfoque similar también se puede utilizar en frameworks PHP como Symfony, Laravel o Nette. Puede que no sea tan flexible como los componentes de React o Vue, pero puedes crear un montón de bloques reutilizables.

Estilizando elementos globalmente

A veces es necesario estilizar elementos globalmente. Como estilizar elementos anidados dentro de un artículo o añadir algún reset CSS.

Esto se puede hacer de varias maneras usando selectores personalizados.

Cuando sólo tienes una pequeña cantidad de selectores o elementos para modificar, está bien darle estilo directamente a través del atributo class.

<nav class="
display:flex flex-direction:column

[a]{display:inline-flex;align-items:center;text-decoration:none}
[a_.icon]{margin-right:4px;line-height:1}
[a_span]{font-size:16px}
[a:hover_span]{text-decoration:underline}
">
	<a href="#">
		<i class="icon"></i>
		<span>Enlace</span>
	</a>
</nav>

Cuando el estilo que tienes que añadir a elementos internos/globales empieza a crecer, es mejor moverlo fuera del atributo class. Puedes definir estos selectores dentro de tu archivo de configuración o en una plantilla donde se utilicen.

En caso de que el estilo se aplique sólo en una plantilla, podemos definirlo dentro de la plantilla. Los selectores personalizados se pueden definir dentro de un comentario en la sección stylify-customSelectors. Se espera un objeto javascript sin corchetes alrededor. La sintaxis dentro de los literales de la plantilla es similar a SCSS. Sin embargo, para mantener las cosas simples, sólo soporta anidamiento y encadenamiento. Las opciones de contenido de los selectores personalizados esperan un objeto javascript sin corchetes:

<!--
stylify-customSelectors
	'.navigation': `
		display:flex flex-direction:column

		a {
			display:inline-flex align-items:center text-decoration:none
			.icon { margin-right:4px line-height:1 }
			span { font-size:16px }
			&:hover span { text-decoration:underline }
		}
	`
/stylify-customSelectors
-->
<nav class="navigation">
	<a href="#">
		<i class="icon"></i>
		<span>Enlace</span>
	</a>
</nav>

La config de una plantilla también se puede mover al archivo config:

const compilerConfig = {
	customSelectors: {
		'.navigation': `
			display:flex flex-direction:column

			a {
				display:inline-flex align-items:center text-decoration:none
				.icon { margin-right:4px line-height:1 }
				span { font-size:16px }
				&:hover span { text-decoration:underline }
			}
		`
	}
}

Eliminando Valores Hardcodeados por Variables

Al principio, parece correcto escribir un selector como color:#000 o box-shadow:0_4px_8px_rgba(0,0,0,.16). Sin embargo, ¿qué pasa si queremos utilizar la misma sombra en toda la web o en algún lugar en el futuro, vamos a querer apoyar un modo claro / oscuro. Esto simplemente no funcionará y complicará el mantenimiento y la refactorización.

En el caso de Stylify, las variables pueden ser definidas de dos maneras. Si una variable es usada globalmente, ponla en el global config:

const compilerConfig = {
	variables: {
		textColor: '#000',
		shadow: '0 4px 8px rgba(0,0,0,.16)'
	}
}

Si se utiliza localmente, configurarlo dentro de un comentario en un archivo donde se utiliza utilizando opciones de contenido. Espera un objeto javascript sin corchetes alrededor:

<!--
stylify-variables
	textColor: '#000',
	shadow: '0 4px 8px rgba(0,0,0,.16)'
/stylify-variables
-->

Puedes reutilizar estas variables en varios sitios de esta forma:

<span class="color:$textColor box-shadow:$shadow">¡Hola Mundo!</span>

Cuando necesite modificar la fuente o el color en función de la pantalla o de las preferencias del tema de color, utilice variables CSS para cambiar de forma flexible los colores, los tamaños de fuente, el fondo y otras propiedades, en función de la pantalla y de la consulta de medios. Esto disminuirá el número de selectores y simplificará la refactorización.

const compilerConfig = {
	// https://stylifycss.com/en/docs/stylify/compiler#variables
	variabels: {
		textFontSize: '12px',
		textColor: '#000',
		// Intenta coincidir con una pantalla, puede ser sm, md, lg...
		minw400px: {
			textFontSize: '18px'
		},
		// Para un @media (prefer-color-scheme: dark)
		dark: {
			textColor: '#fff'
		},
		// Cuando la pantalla no se encuentra,
		// se recurre a un selector personalizado
		// en este caso un elemento con la clase ".dark
		// que muy probablemente será la raíz (html el)
		'.dark': {
			textColor: '#fff'
		},
	}
};

Y en HTML:

<span class="font-size:$textFontSize color:$textColor"></span>

Simplificación del desarrollo mediante hooks

Para eliminar algunas tareas repetitivas, puedes utilizar el sistema de CSS Hooks de Stylify. Stylify proporciona varios ganchos dentro del Compilador, Bundler y Runtime.

Estos ganchos se pueden utilizar para adjuntar oyentes, con los que se puede, por ejemplo:

  • Generar dinámicamente una guía de estilo
  • Modificar/Convertir/Extender valores de nuevas coincidencias de selector
  • Modificar la salida del bundler
  • Activar algún callback dentro de un navegador, cuando Stylify genera el CSS inicial
  • Etc…

Abajo hay un ejemplo de un hook, que escucha una nueva macro para font-size. Este hook comprueba, si el valor del tamaño de la fuente está en el rango permitido y convierte las unidades px a REM. También adjunta la altura de línea correcta para que no tengamos que añadir el selector line-height cada vez que queramos usar el selector font-size:

import { hooks } from '@stylify/stylify';

// 1. Crear una función que trabaje con la propiedad font-size
const fontSizeHandler = ({ selectorProperties }) => {
	const propertyValue = selectorProperties['font-size'];

	// Si el valor de la fuente es por ejemplo un valor por defecto REM, no hacer nada
	// O, si la unidad no son píxeles por ejemplo
	if (propertyValue === '62.5%' || !propertyValue.endsWith('px')) {
		return;
	}

	const numberValue = Number(propertyValue.match(/^\d+/));
	// Creemos los tamaños de fuente permitidos
	const allowedFontSizes = {
		'Small text': 12,
		'Regular text': 16,
		'Large text': 20,
		'Subtitle': 24,
		'Title': 32,
		'Large title': 42,
		'Extra large title': 64
	}

	// Si el desarrollador intenta utilizar, por ejemplo, font-size:13px, se producirá un error
	if (!Object.values(allowedFontSizes).includes(numberValue)) {
		throw new Error(`Font size "${propertyValue}" is not allowed. Available values are "${JSON.stringify(allowedFontSizes)}".`);
	}

	const pixelFontSize = Number(pixelUnit.slice(0,-2));
	const remFontSize = pixelFontSize / 10;

	// Además, recalcular px a rem y añadir una altura de línea correcta
	// Para no tener que añadirla manualmente cada vez
	selectorProperties['font-size'] = `${remFontSize}rem`;
	selectorProperties['line-height'] = `${remFontSize * (pixelFontSize >= 28 ? 1.2 : 1.7)}rem`;

}

// 2. Añádelo a los manejadores
const newMacroMatchHandlers = {
	'font-size': fontSizeHandler
};

// 3. Añade un hook que escuche la nueva macro coincidente, obtenga el manejador apropiado y lo llame
hooks.addListener('compiler:newMacroMatch', ({ macroMatch, selectorProperties }) => {
	const selectorProperties = Object.keys(selectorProperties);
	for (const selectorProperty of selectorProperties) {
		const handler = newMacroMatchHandlers[selectorProperty] ?? undefined;

		if (handler === undefined) {
			continue;
		}

		handler(data);
	}
});

Dividir CSS

Stylify te permite dividir los paquetes CSS básicamente para cada archivo por separado. Esta característica aumenta el nivel de optimización de CSS al siguiente nivel ya que los paquetes pueden llegar a ser muy pequeños.

Dividir CSS para secciones de App

Digamos que tienes 2 secciones de aplicación. Uno para la parte delantera de la página web y otro para el panel de administración / usuario. Para 2 secciones, sólo 2 archivos CSS serán generados. El CSS para el frontend no se cargará en el admin y viceversa.

Dividir CSS para Layouts y páginas

De esta forma tienes una importación de CSS para el layout y otra para la página. Cuando cargues una página, se cargarán el CSS del diseño y el de la página. Lo mismo ocurre con todas las demás páginas, excepto con el CSS del diseño, que ahora se almacena en caché.

Capas y ámbitos CSS

Si decides dividir el CSS en múltiples archivos para maquetaciones/páginas puedes encontrarte con un problema de selectores CSS anulados.

Afortunadamente este problema tiene una fácil solución. Usamos capas CSS y Ámbitos para corregir la especificidad.

Stylify CSS proporciona capas CSS dentro de la configuración del paquete. Esta configuración puede ser usada dentro de Stylify Unplugin (para Next, React, Vue, Vite, SvelteKit, Symfony, Laravel, Nette, etc.) y Stylify Astro (Astro.build). Ambos paquetes utilizan el paquete Stylify Bundler bajo el capó.

Configuración común para Stylify Unplugin, integración con Astro y Bundler:

const bundlerConfig = {
	cssLayersOrder: {
		// Esto generará @layer layout,page;
		order: 'layout, page',
		// Esto le dice a Stylify que exporte el orden de capas anterior
		// sólo en los paquetes, que tiene capa de diseño CSS
		exportLayer: ['layout']
	},
};

const layoutBundle = {
	files: ['path/to/layout.html'],
	outputFile: 'path/to/layout.css',
	cssLayer: 'layout'
};

const pageBundle = {
	files: ['path/to/page.html'],
	outputFile: 'path/to/page.css',
	cssLayer: 'page'
};

const config = {
	bundler: bundlerConfig,
	bundles: [ layoutBundle, pageBundle ]
};

Uso de esta configuración con unplugin:

import { stylifyVite, stylifyWebpack, stylifyEsbuild, stylifyRollup } from '@stylify/unplugin';

stylifyVite(config);
stylifyWebpack(config);
stylifyEsbuild(config);
stylifyRollup(config);

Ejemplo con integración de Astro.build:

import stylify from '@stylify/astro';

export default {
	integrations: [stylify(config)]
}

Ejemplo con uso directo de bundler:

import { Bundler } from '@stylify/bundler';

const bundler = new Bundler(config)
bundler.bundle();

await bundler.waitOnBundlesProcessed();