Apuntes DAM
Volver al inicio

Una biblioteca de componentes con BEM, tokens de diseño y tema oscuro

Ejercicio de HTML y CSSMedioUnos 70 minutos

Construye los componentes de un sistema de diseño como los de Bootstrap o Material: botones con sus variantes y una tarjeta con nombres BEM, todos los valores en tokens (variables CSS) y un tema oscuro que solo redefine los tokens, con el contraste comprobado en los dos temas.

  • Metodología BEM
  • Tokens de diseño con variables CSS
  • Componentes y modificadores
  • Grid con auto-fill y minmax
  • Temas claro y oscuro
  • Contraste WCAG

Enunciado

Un sistema de diseño reúne los componentes de una interfaz (botones, tarjetas, formularios) para que toda la aplicación sea coherente. Se apoya en dos ideas: los tokens de diseño, que guardan cada decisión (el color primario, el radio de las esquinas, la unidad de espacio) en un solo sitio, y una forma de nombrar las clases que evite conflictos, como BEM: bloque, bloque__elemento y bloque--modificador.

Con Sass los tokens serían variables $primario y los modificadores se escribirían anidados con &--secundario; aquí se usan las variables nativas de CSS (--color-primario), que tienen una ventaja que Sass no puede dar: se pueden redefinir en tiempo de ejecución. Por eso el tema oscuro del último apartado no reescribe ningún componente, solo los tokens.

Qué hay que hacer

  1. Cada apartado se corrige por separado con pruebas que se ejecutan dentro de la página (algunas a varios anchos de pantalla). Debajo del editor tienes el resultado en directo.
  2. Los apartados de CSS empiezan con la solución del anterior ya escrita: así la página va creciendo. Puedes hacer los apartados en cualquier orden, pero conviene seguirlo.
  3. Usa siempre los tokens con var() para los colores, los radios y los espacios: las pruebas cambian los tokens y comprueban que los componentes cambian con ellos.

Guía paso a paso

Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.

1. Primero los tokens

Antes de escribir un componente, decide los valores y ponles nombre en :root. Después, en los componentes no debería quedar ni un color ni un tamaño escrito a mano.

css
:root {
  --color-primario: #1d4ed8;
  --espacio: 8px;
}
.boton {
  background: var(--color-primario);
  padding: var(--espacio) calc(var(--espacio) * 2);
}
2. Bloque, elemento y modificador

En BEM, tarjeta es el bloque, tarjeta__titulo una parte que solo tiene sentido dentro de él y tarjeta--destacada una variante. Todos los selectores son de una sola clase: la especificidad es siempre la misma y nada pisa a nada por accidente.

3. Diseño intrínseco

auto-fill con minmax deja que la rejilla decida cuántas columnas caben, y Flexbox en columna dentro de la tarjeta alinea los botones abajo sin medir nada.

4. Temas con variables

Un tema es otro juego de valores para los mismos tokens. Como las variables CSS se heredan, basta con redefinirlas en un ancestro ([data-tema="oscuro"]) para que cambie todo lo que hay dentro.

Resuélvelo aquí

Cada apartado edita un fichero de la página (el HTML, el CSS o el JavaScript) y se corrige con pruebas que se ejecutan en la propia página, también a distintos anchos de pantalla. Debajo del editor ves el resultado en directo.

CSSApartado 1 · CSSMedio

Los tokens y el botón. En :root, estos tokens: --color-primario: #1d4ed8, --color-sobre-primario: #ffffff (el texto sobre el color primario), --color-peligro: #b91c1c, --color-texto: #1f2937, --color-fondo: #ffffff, --color-borde: #e5e7eb, --radio: 6px y --espacio: 8px. El bloque .boton: la fuente heredada (font: inherit), fondo del color primario, texto del color sobre primario, sin borde, esquinas de --radio, relleno vertical de --espacio y horizontal del doble (calc) y cursor de mano. Los modificadores: .boton--secundario (fondo transparente, texto y borde de 1px del color primario), .boton--peligro (fondo del color de peligro) y .boton--pequeno (relleno de 4px y 8px y font-size: 0.875rem). Un botón desactivado (:disabled), con opacidad 0,5 y el cursor not-allowed.

index.htmlsolo lectura
estilos.css
Vista previa
CSSApartado 2 · CSSMedio

La tarjeta y la rejilla. .rejilla: una Grid con repeat(auto-fill, minmax(220px, 1fr)), 16px de separación y 16px de relleno. El bloque .tarjeta: posición relativa, Flexbox en columna, borde de 1px del color de borde, esquinas del doble de --radio, overflow: hidden, fondo del color de fondo y texto del color de texto. Sus elementos: __imagen (bloque, todo el ancho, proporción 16/9 y object-fit: cover), __cuerpo (crece para ocupar el sitio libre, con relleno del doble de --espacio), __titulo (sin margen salvo --espacio abajo, 1.25rem), __texto (sin márgenes), __acciones (Flexbox con el contenido a la derecha y relleno del doble de --espacio a los lados y abajo) y __etiqueta (en la esquina superior derecha, a --espacio de los bordes, con relleno de 2px y 8px, esquinas de 999px, fondo del color de peligro, texto del color sobre primario y 0.75rem). Y el modificador .tarjeta--destacada, con un borde de 2px del color primario.

index.htmlsolo lectura
estilos.css
Vista previa
CSSApartado 3 · CSSMedio

El tema oscuro. Cuando el elemento html tenga data-tema="oscuro", redefine solo los tokens: --color-primario: #93c5fd, --color-sobre-primario: #111827, --color-peligro: #f87171, --color-texto: #f9fafb, --color-fondo: #1f2937 y --color-borde: #374151. Haz además que el body use los tokens de fondo y de texto. No cambies ninguna regla de los componentes.

index.htmlsolo lectura
estilos.css
Vista previa

Solución explicada

Ver las soluciones de todos los apartados

Apartado 1 · CSS

css
:root {
  --color-primario: #1d4ed8;
  --color-sobre-primario: #ffffff;
  --color-peligro: #b91c1c;
  --color-texto: #1f2937;
  --color-fondo: #ffffff;
  --color-borde: #e5e7eb;
  --radio: 6px;
  --espacio: 8px;
}
body { margin: 0; font-family: system-ui, sans-serif; }
.fila { display: flex; flex-wrap: wrap; gap: 8px; padding: 16px; }

/* Bloque: boton */
.boton {
  font: inherit;
  background: var(--color-primario);
  color: var(--color-sobre-primario);
  border: none;
  border-radius: var(--radio);
  padding: var(--espacio) calc(var(--espacio) * 2);
  cursor: pointer;
}
/* Modificadores */
.boton--secundario {
  background: transparent;
  color: var(--color-primario);
  border: 1px solid var(--color-primario);
}
.boton--peligro { background: var(--color-peligro); }
.boton--pequeno { padding: 4px 8px; font-size: 0.875rem; }
.boton:disabled { opacity: 0.5; cursor: not-allowed; }

Apartado 2 · CSS

css
:root {
  --color-primario: #1d4ed8;
  --color-sobre-primario: #ffffff;
  --color-peligro: #b91c1c;
  --color-texto: #1f2937;
  --color-fondo: #ffffff;
  --color-borde: #e5e7eb;
  --radio: 6px;
  --espacio: 8px;
}
body { margin: 0; font-family: system-ui, sans-serif; }
.fila { display: flex; flex-wrap: wrap; gap: 8px; padding: 16px; }

/* Bloque: boton */
.boton {
  font: inherit;
  background: var(--color-primario);
  color: var(--color-sobre-primario);
  border: none;
  border-radius: var(--radio);
  padding: var(--espacio) calc(var(--espacio) * 2);
  cursor: pointer;
}
/* Modificadores */
.boton--secundario {
  background: transparent;
  color: var(--color-primario);
  border: 1px solid var(--color-primario);
}
.boton--peligro { background: var(--color-peligro); }
.boton--pequeno { padding: 4px 8px; font-size: 0.875rem; }
.boton:disabled { opacity: 0.5; cursor: not-allowed; }

.rejilla {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 16px;
  padding: 16px;
}

/* Bloque: tarjeta */
.tarjeta {
  position: relative;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--color-borde);
  border-radius: calc(var(--radio) * 2);
  overflow: hidden;
  background: var(--color-fondo);
  color: var(--color-texto);
}
.tarjeta__imagen { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
.tarjeta__cuerpo { flex: 1; padding: calc(var(--espacio) * 2); }
.tarjeta__titulo { margin: 0 0 var(--espacio); font-size: 1.25rem; }
.tarjeta__texto { margin: 0; }
.tarjeta__acciones {
  display: flex;
  justify-content: flex-end;
  padding: 0 calc(var(--espacio) * 2) calc(var(--espacio) * 2);
}
.tarjeta__etiqueta {
  position: absolute;
  top: var(--espacio);
  right: var(--espacio);
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--color-peligro);
  color: var(--color-sobre-primario);
  font-size: 0.75rem;
}
.tarjeta--destacada { border: 2px solid var(--color-primario); }

Apartado 3 · CSS

css
:root {
  --color-primario: #1d4ed8;
  --color-sobre-primario: #ffffff;
  --color-peligro: #b91c1c;
  --color-texto: #1f2937;
  --color-fondo: #ffffff;
  --color-borde: #e5e7eb;
  --radio: 6px;
  --espacio: 8px;
}
body { margin: 0; font-family: system-ui, sans-serif; }
.fila { display: flex; flex-wrap: wrap; gap: 8px; padding: 16px; }

/* Bloque: boton */
.boton {
  font: inherit;
  background: var(--color-primario);
  color: var(--color-sobre-primario);
  border: none;
  border-radius: var(--radio);
  padding: var(--espacio) calc(var(--espacio) * 2);
  cursor: pointer;
}
/* Modificadores */
.boton--secundario {
  background: transparent;
  color: var(--color-primario);
  border: 1px solid var(--color-primario);
}
.boton--peligro { background: var(--color-peligro); }
.boton--pequeno { padding: 4px 8px; font-size: 0.875rem; }
.boton:disabled { opacity: 0.5; cursor: not-allowed; }

.rejilla {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 16px;
  padding: 16px;
}

/* Bloque: tarjeta */
.tarjeta {
  position: relative;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--color-borde);
  border-radius: calc(var(--radio) * 2);
  overflow: hidden;
  background: var(--color-fondo);
  color: var(--color-texto);
}
.tarjeta__imagen { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
.tarjeta__cuerpo { flex: 1; padding: calc(var(--espacio) * 2); }
.tarjeta__titulo { margin: 0 0 var(--espacio); font-size: 1.25rem; }
.tarjeta__texto { margin: 0; }
.tarjeta__acciones {
  display: flex;
  justify-content: flex-end;
  padding: 0 calc(var(--espacio) * 2) calc(var(--espacio) * 2);
}
.tarjeta__etiqueta {
  position: absolute;
  top: var(--espacio);
  right: var(--espacio);
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--color-peligro);
  color: var(--color-sobre-primario);
  font-size: 0.75rem;
}
.tarjeta--destacada { border: 2px solid var(--color-primario); }

/* Tema oscuro: solo se redefinen los tokens */
[data-tema="oscuro"] {
  --color-primario: #93c5fd;
  --color-sobre-primario: #111827;
  --color-peligro: #f87171;
  --color-texto: #f9fafb;
  --color-fondo: #1f2937;
  --color-borde: #374151;
}
body { background: var(--color-fondo); color: var(--color-texto); }

BEM resuelve el mayor problema del CSS en equipos grandes: los conflictos entre reglas. Con nombres únicos por componente y selectores de una sola clase, puedes cambiar .tarjeta__titulo sabiendo que no afecta a ningún otro título de la web.

Los tokens hacen que el diseño sea una decisión centralizada: si la marca cambia su azul, se cambia una línea. Es lo que hacen Bootstrap 5 (--bs-primary), Material Design o Tailwind con su configuración.

El tema oscuro no es invertir colores: los azules se aclaran, los fondos no son negros puros y el texto de los botones pasa a ser oscuro para mantener el contraste. Con los tokens, esas decisiones se toman una vez y valen para todos los componentes.

En Sass, lo mismo se escribiría con variables $ y anidando los elementos con &__titulo y los modificadores con &--destacada; al compilar sale este mismo CSS. Las variables de Sass desaparecen al compilar, así que para cambiar de tema en el navegador hacen falta las variables de CSS.

Para ir más allá

  • Haz que el tema oscuro se aplique también con @media (prefers-color-scheme: dark) si el usuario no ha elegido ninguno.
  • Añade el componente alerta con sus modificadores --exito y --error usando solo tokens.
  • Reescribe la hoja en Sass con anidamiento BEM (&__elemento, &--modificador) y compílala.

Dónde se explica