Tema
Arquitectura CSS (BEM)
Escribir CSS que funcione es fácil; escribir CSS que siga funcionando cuando el proyecto crece a 30 páginas, tres desarrolladores y dos años de cambios del cliente es otra historia. En esta lección aprenderás a ponerle nombre y orden a tus estilos con BEM, a organizar tus archivos por capas (base, layout, componentes, utilidades), a usar ITCSS y @layer para controlar la cascada, y a automatizar el estilo del código con Stylelint y Prettier.
Concepto
El problema del CSS en proyectos grandes no es la sintaxis, es el caos:
- Cambias el color de
.tituloen la página de inicio y se rompe el blog. - Hay selectores como
#main .content div ul li a spanimposibles de sobrescribir. - Nadie se atreve a borrar CSS "por si acaso", así que el archivo crece sin parar.
Una arquitectura CSS es un conjunto de reglas de equipo: cómo se nombran las clases, dónde vive cada estilo y qué está prohibido. No es una librería que instalas: es una forma de trabajar.
Analogía
Piensa en una cocina profesional. Cualquier cocinero nuevo sabe dónde están los cuchillos, las especias y los platos, porque todo tiene un lugar y una etiqueta. Una arquitectura CSS hace lo mismo con tus estilos: cualquier persona del equipo sabe dónde buscar y cómo nombrar algo nuevo sin romper lo existente.
BEM: Bloque, Elemento, Modificador
BEM es la convención de nombres más usada en la industria. Divide la interfaz en tres tipos de piezas:
| Pieza | Qué es | Sintaxis | Ejemplo |
|---|---|---|---|
| Bloque | Componente independiente y reutilizable | .bloque | .tarjeta, .menu, .boton |
| Elemento | Parte de un bloque que no tiene sentido fuera de él | .bloque__elemento | .tarjeta__titulo, .menu__enlace |
| Modificador | Variante o estado de un bloque o elemento | .bloque--modificador | .boton--primario, .tarjeta--destacada |
- Dos guiones bajos
__separan bloque y elemento. - Dos guiones medios
--indican un modificador. - Las palabras compuestas se separan con un guion:
.formulario-contacto__campo-texto.
Ejemplo completo
html
<!-- Bloque: tarjeta -->
<article class="tarjeta tarjeta--destacada">
<img class="tarjeta__imagen" src="img/curso.webp" alt="Curso de CSS">
<div class="tarjeta__cuerpo">
<span class="tarjeta__etiqueta">Nuevo</span>
<h3 class="tarjeta__titulo">Curso de CSS</h3>
<p class="tarjeta__texto">De cero a profesional en 20 lecciones.</p>
<!-- Otro bloque anidado: boton (tiene sus propias clases) -->
<a class="boton boton--primario" href="#">Inscribirme</a>
</div>
</article>css
/* ===== Bloque ===== */
.tarjeta {
display: flex;
flex-direction: column;
border-radius: 12px;
background: #fff;
overflow: hidden;
box-shadow: 0 2px 8px rgb(0 0 0 / 0.08);
}
/* ===== Elementos: siempre con una sola clase ===== */
.tarjeta__imagen { aspect-ratio: 16 / 9; object-fit: cover; width: 100%; }
.tarjeta__cuerpo { display: flex; flex-direction: column; gap: 0.5rem; padding: 1.25rem; }
.tarjeta__etiqueta { align-self: flex-start; font-size: 0.75rem; color: #7c3aed; font-weight: 700; }
.tarjeta__titulo { font-size: 1.15rem; margin: 0; }
.tarjeta__texto { color: #64748b; margin: 0; }
/* ===== Modificador: sólo cambia lo necesario ===== */
.tarjeta--destacada {
outline: 2px solid #7c3aed;
}
.tarjeta--destacada .tarjeta__titulo {
color: #7c3aed; /* el modificador del bloque puede afectar a sus elementos */
}Fíjate en las ventajas:
- Especificidad baja y plana: casi todo es una sola clase (0,1,0), así que nada "gana" por accidente.
- Autodocumentado: al ver
tarjeta__tituloen el HTML sabes exactamente en qué archivo y bloque buscar. - Sin colisiones:
.tarjeta__tituloy.modal__titulonunca chocan.
Juega con los modificadores en vivo:
Modificadores de estado
Para estados temporales (abierto, activo, cargando) hay dos estilos válidos. Elige uno y úsalo siempre:
css
/* Opción A: modificador BEM */
.menu--abierto { display: block; }
/* Opción B: clases de estado con prefijo is- / has- (muy común con JavaScript) */
.menu.is-abierto { display: block; }
/* Opción C (la mejor cuando existe): atributos de accesibilidad */
.menu__boton[aria-expanded="true"] + .menu__lista { display: block; }Buena práctica
Si ya necesitas un atributo ARIA para la accesibilidad (aria-expanded, aria-current, aria-selected), úsalo también como selector de estilo. Así el estado visual y el estado accesible nunca se desincronizan.
Errores típicos con BEM
Error común 1: elementos de elementos
.tarjeta__cuerpo__titulo está mal. BEM no refleja la estructura del DOM. Aunque el título esté dentro del cuerpo, sigue siendo un elemento del bloque: .tarjeta__titulo.
Error común 2: modificador sin la clase base
<button class="boton--primario"> está mal: el modificador sólo contiene las diferencias. Siempre van juntas: class="boton boton--primario".
Otros errores frecuentes:
- Anidar selectores sin necesidad:
.tarjeta .tarjeta__titulosube la especificidad sin aportar nada. Escribe sólo.tarjeta__titulo. - Estilar etiquetas dentro de bloques:
.menu a { }afectará a cualquier enlace que alguien meta ahí después. Mejor.menu__enlace. - Bloques que se posicionan a sí mismos: un bloque no debería tener
marginexterior niposition: absoluterespecto a la página, porque deja de ser reutilizable. El espaciado lo decide el contenedor padre (por ejemplo congap). - Nombres por apariencia:
.texto-rojoo.caja-izquierdadejan de tener sentido cuando el diseño cambia. Nombra por función:.alerta__mensaje,.layout__lateral.
Mezclas (mixes)
Un mismo nodo HTML puede ser elemento de un bloque y a la vez otro bloque. Es la forma correcta de posicionar un bloque dentro de otro:
html
<header class="header">
<!-- "boton" da el aspecto; "header__cta" da la posición dentro del header -->
<a class="boton boton--primario header__cta" href="#contacto">Cotizar</a>
</header>css
.header__cta { margin-left: auto; } /* posición: responsabilidad del header */Organización de archivos
Cuando el CSS pasa de unas 300 líneas, divídelo por responsabilidad. Una estructura sencilla y profesional:
txt
css/
├── base/
│ ├── reset.css ← normalización y box-sizing
│ ├── variables.css ← colores, tipografía, espaciados (tokens)
│ └── tipografia.css ← estilos de h1-h6, p, a, listas
├── layout/
│ ├── contenedor.css ← .contenedor, anchos máximos
│ ├── header.css
│ └── footer.css
├── componentes/
│ ├── boton.css ← un archivo por bloque BEM
│ ├── tarjeta.css
│ └── formulario.css
├── utilidades/
│ └── utilidades.css ← .sr-only, .text-center, .mt-0...
└── main.css ← importa todo en ordencss
/* main.css: el orden importa, de lo más general a lo más específico */
@import url("base/reset.css");
@import url("base/variables.css");
@import url("base/tipografia.css");
@import url("layout/contenedor.css");
@import url("layout/header.css");
@import url("layout/footer.css");
@import url("componentes/boton.css");
@import url("componentes/tarjeta.css");
@import url("componentes/formulario.css");
@import url("utilidades/utilidades.css");Rendimiento
@import de CSS puro genera una petición en cadena por archivo. Está bien en desarrollo, pero en producción conviene unir todo en un solo archivo con una herramienta (Vite, Sass o PostCSS lo hacen automáticamente). Lo verás en la lección de Sass.
ITCSS: el triángulo invertido
ITCSS (Inverted Triangle CSS) ordena los estilos de lo más genérico y de mayor alcance a lo más específico y de menor alcance. Así la especificidad crece de forma natural y casi nunca necesitas !important.
txt
▼ alcance amplio, especificidad baja
┌────────────────────────────────┐
│ 1. Settings variables │
│ 2. Tools mixins (Sass) │
│ 3. Generic reset, box-sizing │
│ 4. Elements h1, a, p │
│ 5. Objects .contenedor, .grid│
│ 6. Components .tarjeta, .boton │
│ 7. Utilities .sr-only, .mt-0 │
└────────────────────────────────┘
▲ alcance reducido, especificidad altaLas utilidades van al final a propósito: una clase como .oculto debe ganarle a cualquier componente.
@layer: control nativo de la cascada
@layer (soportado por todos los navegadores modernos) lleva la idea de ITCSS al propio CSS. Declaras el orden de las capas una vez y el orden de las capas gana sobre la especificidad:
css
/* 1. Declara el orden: la última capa tiene prioridad */
@layer reset, base, layout, componentes, utilidades;
@layer reset {
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; }
}
@layer base {
a { color: var(--color-primario); }
}
@layer componentes {
/* Aunque este selector es muy específico (#id + clase)... */
#sidebar .menu__enlace { color: #334155; }
}
@layer utilidades {
/* ...esta clase simple le gana, porque su capa va después */
.texto-blanco { color: #fff; }
}También puedes meter un archivo completo en una capa al importarlo, ideal para librerías de terceros:
css
@layer reset, terceros, componentes;
@import url("vendor/libreria-slider.css") layer(terceros);Buena práctica
Los estilos sin capa tienen más prioridad que cualquier capa. Úsalo a tu favor: pon todo tu sistema en capas y deja fuera sólo los arreglos puntuales. Profundizaremos en @layer en CSS moderno.
Convenciones de equipo
Una arquitectura sólo funciona si todo el equipo la respeta. Documenta en el README del proyecto reglas como estas:
- Clases en BEM, en minúsculas y con guiones. Nada de
camelCasenisnake_case. - Prohibido estilar con IDs. Los IDs son para anclas y JavaScript.
- Máximo un nivel de anidación (salvo modificadores y estados).
- Un bloque por archivo, con el mismo nombre:
.tarjetavive entarjeta.css. - Prefijo
js-para clases que usa JavaScript (.js-abrir-menu) y que nunca llevan estilos. Así nadie las borra creyendo que son decorativas. - Valores con variables: colores, espaciados y radios siempre desde variables CSS.
!importantsólo en utilidades, nunca en componentes.- Orden de propiedades consistente: posición, caja, tipografía, visual, misceláneo (o alfabético, pero el mismo para todos).
Automatiza: Stylelint y Prettier
Las reglas que dependen de la memoria se olvidan. Deja que las herramientas las revisen por ti.
- Prettier formatea el código (sangría, comillas, saltos de línea). No discute: lo deja siempre igual.
- Stylelint detecta errores y malas prácticas (selectores duplicados, IDs, colores inválidos, nombres que no siguen BEM).
bash
# Instalar en el proyecto (requiere Node.js)
npm install --save-dev prettier stylelint stylelint-config-standardjson
{
"extends": ["stylelint-config-standard"],
"rules": {
"selector-max-id": 0,
"selector-max-compound-selectors": 3,
"declaration-no-important": true,
"selector-class-pattern": [
"^[a-z][a-z0-9]*(-[a-z0-9]+)*(__[a-z0-9]+(-[a-z0-9]+)*)?(--[a-z0-9]+(-[a-z0-9]+)*)?$",
{ "message": "Usa nombres BEM: bloque__elemento--modificador" }
]
}
}Guarda lo anterior como .stylelintrc.json y añade scripts a tu package.json:
json
{
"scripts": {
"lint:css": "stylelint \"css/**/*.css\"",
"format": "prettier --write \"css/**/*.css\""
}
}bash
npm run lint:css # muestra los problemas
npm run format # formatea todos los archivosInstala también las extensiones Stylelint y Prettier en VS Code y activa "Format on Save": verás los errores subrayados mientras escribes.
Caso real: refactorizar CSS desordenado a BEM
Un cliente te pasa este bloque de precios hecho "a la carrera". Funciona, pero nadie se atreve a tocarlo:
html
<div id="precios">
<div class="box">
<h3>Pro</h3>
<div class="price"><span>$499</span> /mes</div>
<ul>
<li>10 páginas</li>
<li>Soporte prioritario</li>
</ul>
<a href="#" class="btn red big">Contratar</a>
</div>
</div>css
/* ANTES: especificidad alta, nombres por apariencia y dependencia de etiquetas */
#precios .box { background: white; padding: 20px; border: 1px solid #ddd; }
#precios .box h3 { font-size: 22px; margin: 0; }
#precios .box div span { font-size: 40px; font-weight: bold; }
#precios .box ul li { padding: 5px 0; border-bottom: 1px solid #eee; }
.red { background: red !important; color: white; }
.big { padding: 15px 30px; }
#precios .box.special { border-color: red; transform: scale(1.05); }Problemas: el ID hace imposible reutilizar la tarjeta fuera de #precios; div span se rompe si cambias el HTML; .red deja de tener sentido si la marca pasa a azul; y !important contagia a todo el proyecto.
html
<!-- DESPUÉS -->
<section class="precios">
<article class="plan plan--destacado">
<h3 class="plan__nombre">Pro</h3>
<p class="plan__precio">
<span class="plan__monto">$499</span> /mes
</p>
<ul class="plan__lista">
<li class="plan__beneficio">10 páginas</li>
<li class="plan__beneficio">Soporte prioritario</li>
</ul>
<a href="#" class="boton boton--primario boton--grande plan__cta">Contratar</a>
</article>
</section>css
/* componentes/plan.css */
.plan {
display: flex;
flex-direction: column;
gap: var(--espacio-3, 1rem);
padding: var(--espacio-4, 1.25rem);
border: 1px solid var(--color-borde, #ddd);
border-radius: var(--radio, 12px);
background: var(--color-superficie, #fff);
}
.plan__nombre { margin: 0; font-size: 1.375rem; }
.plan__precio { margin: 0; color: var(--color-texto-suave, #64748b); }
.plan__monto { font-size: 2.5rem; font-weight: 700; color: var(--color-texto, #0f172a); }
.plan__lista { margin: 0; padding: 0; list-style: none; }
.plan__beneficio { padding: 0.35rem 0; border-bottom: 1px solid var(--color-borde-suave, #eee); }
.plan__cta { margin-top: auto; } /* mezcla: el plan decide dónde va el botón */
.plan--destacado {
border-color: var(--color-primario, #7c3aed);
box-shadow: 0 10px 30px rgb(124 58 237 / 0.18);
}
/* componentes/boton.css */
.boton { display: inline-block; padding: 0.6rem 1.2rem; border-radius: 999px; text-align: center; text-decoration: none; }
.boton--primario { background: var(--color-primario, #7c3aed); color: #fff; }
.boton--grande { padding: 0.9rem 1.8rem; font-size: 1.1rem; }Resultado: todos los selectores tienen especificidad (0,1,0), el plan funciona en cualquier página, el botón es reutilizable y cambiar la marca es cambiar una variable.
Ejercicios
- Convierte a BEM este HTML de un menú:
<nav id="nav"><ul><li class="active"><a>Inicio</a></li></ul></nav>. Incluye un modificador para el enlace activo. - ¿Qué está mal en
.form__group__input--error? Corrígelo. - Declara cuatro capas (
reset,base,componentes,utilidades) y demuestra con un ejemplo que una utilidad de una sola clase le gana a un selector con ID dentro decomponentes. - Escribe la estructura de carpetas para un sitio con header, footer, botones, tarjetas, formulario y una utilidad
.sr-only.
Ver solución
html
<!-- 1 -->
<nav class="nav">
<ul class="nav__lista">
<li class="nav__item">
<a class="nav__enlace nav__enlace--activo" href="/" aria-current="page">Inicio</a>
</li>
</ul>
</nav>css
/* 1 */
.nav__lista { display: flex; gap: 1rem; margin: 0; padding: 0; list-style: none; }
.nav__enlace { color: #334155; text-decoration: none; }
.nav__enlace--activo { color: #7c3aed; font-weight: 700; }
/* 2: no existen "elementos de elementos". El input es elemento del bloque form */
.form__input--error { border-color: #dc2626; }
/* 3 */
@layer reset, base, componentes, utilidades;
@layer componentes {
#principal .alerta { display: block; } /* especificidad (1,1,0) */
}
@layer utilidades {
.oculto { display: none; } /* (0,1,0), pero gana por la capa */
}txt
4
css/
├── base/reset.css
├── base/variables.css
├── layout/header.css
├── layout/footer.css
├── componentes/boton.css
├── componentes/tarjeta.css
├── componentes/formulario.css
├── utilidades/utilidades.css ← aquí va .sr-only
└── main.cssResumen
- Una arquitectura CSS son reglas de equipo para nombrar y ordenar estilos.
- BEM:
.bloque,.bloque__elemento,.bloque--modificador. Nunca elementos de elementos. - Los modificadores siempre acompañan a la clase base y sólo contienen diferencias.
- Organiza por responsabilidad: base, layout, componentes y utilidades; un bloque por archivo.
- ITCSS ordena de lo general a lo específico;
@layerhace que el orden de capas gane a la especificidad. - Nada de IDs para estilos, poco anidamiento y
!importantsólo en utilidades. - Automatiza con Stylelint y Prettier.
Siguiente: Sass.