Skip to content

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 .titulo en la página de inicio y se rompe el blog.
  • Hay selectores como #main .content div ul li a span imposibles 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:

PiezaQué esSintaxisEjemplo
BloqueComponente independiente y reutilizable.bloque.tarjeta, .menu, .boton
ElementoParte de un bloque que no tiene sentido fuera de él.bloque__elemento.tarjeta__titulo, .menu__enlace
ModificadorVariante 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__titulo en el HTML sabes exactamente en qué archivo y bloque buscar.
  • Sin colisiones: .tarjeta__titulo y .modal__titulo nunca chocan.

Juega con los modificadores en vivo:

Componente tarjeta con BEM

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__titulo sube 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 margin exterior ni position: absolute respecto a la página, porque deja de ser reutilizable. El espaciado lo decide el contenedor padre (por ejemplo con gap).
  • Nombres por apariencia: .texto-rojo o .caja-izquierda dejan 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 orden
css
/* 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 alta

Las 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:

  1. Clases en BEM, en minúsculas y con guiones. Nada de camelCase ni snake_case.
  2. Prohibido estilar con IDs. Los IDs son para anclas y JavaScript.
  3. Máximo un nivel de anidación (salvo modificadores y estados).
  4. Un bloque por archivo, con el mismo nombre: .tarjeta vive en tarjeta.css.
  5. Prefijo js- para clases que usa JavaScript (.js-abrir-menu) y que nunca llevan estilos. Así nadie las borra creyendo que son decorativas.
  6. Valores con variables: colores, espaciados y radios siempre desde variables CSS.
  7. !important sólo en utilidades, nunca en componentes.
  8. 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-standard
json
{
  "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 archivos

Instala 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 ​

  1. 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.
  2. ¿Qué está mal en .form__group__input--error? Corrígelo.
  3. 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 de componentes.
  4. 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.css

Resumen ​

  • 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; @layer hace que el orden de capas gane a la especificidad.
  • Nada de IDs para estilos, poco anidamiento y !important sólo en utilidades.
  • Automatiza con Stylelint y Prettier.

Siguiente: Sass.

Academia Web — aprende, copia y construye.