Saltar al contenido principal
Leyendo: CSS moderno esencial
Apuntes de clase

Clase 30 · CSS moderno esencial

Las novedades de CSS que más han cambiado la forma de escribir estilos: selectores modernos, CSS Nesting, container queries y el selector :has(). Cada bloque termina con su demo en funcionamiento.

01 · Selectores modernos

Más allá de :nth-child

En la clase 12 vimos los selectores descendientes y :nth-child(). Ahora vamos a ver los que nos faltan.

  • :not(): todo menos lo indicado.
  • :is() y :where(): agrupar selectores sin repetir.
  • :first-of-type, :last-of-type, :nth-of-type(): contar solo elementos del mismo tipo.
  • :empty: elementos sin contenido.

Todos están soportados por los navegadores actuales (Chrome, Edge, Firefox y Safari).

01 · Selectores modernos

:not()

Aplica los estilos a todo lo que no cumpla el selector.

Dentro del paréntesis ponemos lo que queremos excluir. Evita crear una clase «contraria» para cada caso.

index.html
<ul class="lista">
  <li>Manzana</li>
  <li class="agotado">Pera</li>
  <li>Uva</li>
</ul>
style.css
.lista li:not(.agotado) {
  background-color: #e6f5d6;
}
01 · Selectores modernos

:is() y :where()

Agrupan varios selectores en uno.

:is() evita repetir el prefijo en cada selector de una lista.

style.css · :is()
/* Sin :is() */
.texto h2,
.texto p {
  font-family: Georgia, serif;
}

/* Con :is() */
.texto :is(h2, p) {
  font-family: Georgia, serif;
}

:where() hace lo mismo, pero su especificidad es 0. Es ideal para estilos base que luego podamos sobrescribir fácilmente (recuerda la clase 6 de especificidad).

style.css · :where()
:where(.texto) span {
  color: crimson;
}
01 · Selectores modernos

:first-of-type, :last-of-type, :nth-of-type()

Como :nth-child(), pero contando solo elementos de la misma etiqueta.

En el HTML de la demo hay un <h2>, un <span> y tres <p>. Con p:first-of-type seleccionamos el primer párrafo aunque no sea el primer hijo.

style.css
.texto p:first-of-type {
  font-weight: 700;
}
.texto p:last-of-type {
  color: #6ea832;
}
.texto p:nth-of-type(2) {
  font-style: italic;
}

:empty selecciona los elementos sin ningún contenido (ni texto):

style.css · :empty
.lista li:empty::before {
  content: "(vacío)";
  color: #999;
}
01 · Selectores modernos

Selectores en funcionamiento

Código completo y resultado.

index.html
<ul class="lista">
  <li>Manzana</li>
  <li class="agotado">Pera</li>
  <li>Uva</li>
  <li class="agotado">Kiwi</li>
  <li></li>
</ul>

<article class="texto">
  <h2>Título del artículo</h2>
  <p>Primer párrafo</p>
  <span>Una nota</span>
  <p>Segundo párrafo</p>
  <p>Tercer párrafo</p>
</article>
style.css
.lista .agotado {
  background-color: #fbeaea;
  text-decoration: line-through;
}
.lista li:not(.agotado) {
  background-color: #e6f5d6;
}
.texto :is(h2, p) {
  font-family: Georgia, serif;
}
:where(.texto) span {
  color: crimson;
}
.texto p:first-of-type {
  font-weight: 700;
}
.texto p:last-of-type {
  color: #6ea832;
}
.texto p:nth-of-type(2) {
  font-style: italic;
}
.lista li:empty::before {
  content: "(vacío)";
  color: #999;
}
Resultado: selectores modernos
02 · CSS Nesting

Anidar reglas en CSS

Escribir los selectores hijos dentro de la regla del padre, como en Sass, pero con CSS nativo.

Hasta ahora, para dar estilo a los elementos de una tarjeta repetíamos .card en cada selector. Con nesting los metemos dentro de la llave de .card.

style.css · antes
.card h3 {
  color: #364f6b;
}
.card p {
  color: #555;
}
.card:hover {
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
}
style.css · con nesting
.card {
  h3 {
    color: #364f6b;
  }
  p {
    color: #555;
  }
  &:hover {
    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
  }
}

Se puede escribir h3 { } directamente dentro de la regla. Los navegadores anteriores a 2023 necesitaban & h3.

02 · CSS Nesting

El símbolo &

Representa al selector del padre.

Lo usamos cuando queremos unir el padre con una pseudoclase u otra clase, o cuando el padre debe ir detrás.

style.css
.boton {
  background-color: #92d050;

  /* .boton:hover */
  &:hover {
    background-color: #6ea832;
  }

  /* .boton.grande (la misma etiqueta con 2 clases) */
  &.grande {
    padding: 16px 28px;
  }

  /* .oscuro .boton (el & va detrás) */
  .oscuro & {
    background-color: #1c2b0d;
  }
}
02 · CSS Nesting

Anidar @media

El @media dentro de la regla que modifica.

Podemos poner el @media dentro de la regla: así vemos junto el estilo normal y su cambio responsive, sin buscarlo al final del archivo.

style.css
.card {
  padding: 20px;

  @media (max-width: 500px) {
    padding: 12px;
  }
}
02 · CSS Nesting

Nesting en funcionamiento

Dos tarjetas con hover, botón y versión destacada, todo anidado en .card.

index.html
<div class="tarjetas">
  <article class="card">
    <h3>Tarjeta normal</h3>
    <p>Pasa el ratón por encima.</p>
    <a class="boton" href="#">Ver más</a>
  </article>

  <article class="card destacada">
    <h3>Tarjeta destacada</h3>
    <p>Lleva la clase destacada.</p>
    <a class="boton" href="#">Ver más</a>
  </article>
</div>
style.css
.card {
  padding: 20px;
  background-color: #fff;
  border: 1px solid #d8e0e9;
  border-radius: 12px;
  transition: 0.3s;

  h3 {
    margin: 0 0 8px;
    color: #364f6b;
  }
  p {
    margin: 0 0 14px;
    color: #555;
  }
  .boton {
    display: inline-block;
    padding: 8px 14px;
    background-color: #92d050;
    color: #1c2b0d;
    border-radius: 8px;
    text-decoration: none;

    &:hover {
      background-color: #6ea832;
    }
  }
  &:hover {
    transform: translateY(-4px);
    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
  }
  &.destacada {
    border-color: #92d050;
    border-width: 2px;
  }
  @media (max-width: 500px) {
    padding: 12px;
  }
}
Resultado: CSS Nesting
03 · Container queries

Media query vs container query

Un componente que responde al espacio que tiene, no al tamaño de la pantalla.

Una media query mira el ancho del navegador. Pero la misma tarjeta puede estar en una columna estrecha o en una zona ancha dentro de la misma pantalla. Con las container queries la tarjeta mira el ancho de su contenedor.

style.css
/* Media query: depende del ancho de la PANTALLA */
@media (min-width: 800px) {
  .tarjeta {
    display: grid;
  }
}

/* Container query: depende del ancho del CONTENEDOR */
@container (min-width: 400px) {
  .tarjeta {
    display: grid;
  }
}

Continúa la clase 25 (media queries) y la clase 26 (contenedores flexibles).

03 · Container queries

Definir el contenedor

Marcamos qué elemento actúa como contenedor.

  • container-type: inline-size: el contenedor se puede consultar por su ancho.
  • container-name: nombre opcional para referirnos a él desde @container.
style.css
.caja {
  container-type: inline-size;
  container-name: tarjeta;
}

El elemento marcado como contenedor no puede cambiar con su propia query: lo que cambia son sus hijos.

03 · Container queries

Unidades de contenedor (cqw)

1cqw es el 1% del ancho del contenedor.

Funcionan como vw (clase 27), pero relativas al contenedor. Se combinan muy bien con clamp().

style.css
.tarjeta h3 {
  font-size: clamp(1rem, 5cqw, 1.6rem);
}
03 · Container queries

Container query en funcionamiento

La misma tarjeta dentro de dos cajas de distinto ancho.

index.html
<div class="caja estrecha">
  <article class="tarjeta">
    <div class="foto"></div>
    <div>
      <h3>Ruta de montaña</h3>
      <p>La misma tarjeta cambia de diseño según el ancho de su contenedor.</p>
    </div>
  </article>
</div>

<div class="caja ancha">
  <article class="tarjeta">
    <div class="foto"></div>
    <div>
      <h3>Ruta de montaña</h3>
      <p>La misma tarjeta cambia de diseño según el ancho de su contenedor.</p>
    </div>
  </article>
</div>
style.css
.caja {
  container-type: inline-size;
  container-name: tarjeta;
}
.estrecha {
  width: 260px;
}
.ancha {
  width: 560px;
}
.tarjeta {
  display: grid;
  gap: 12px;
  padding: 12px;
  background-color: #f6f8fa;
  border: 1px solid #d8e0e9;
  border-radius: 12px;
}
.foto {
  height: 90px;
  border-radius: 8px;
  background-image: linear-gradient(135deg, #3a8dde, #bfe6ff);
}
.tarjeta h3 {
  margin: 0 0 6px;
  font-size: 1rem;
}
.tarjeta p {
  margin: 0;
  font-size: 0.9rem;
}

@container tarjeta (min-width: 400px) {
  .tarjeta {
    grid-template-columns: 160px 1fr;
    align-items: center;
  }
  .tarjeta h3 {
    font-size: 1.4rem;
  }
  .foto {
    height: 120px;
  }
}
Resultado: container query
04 · El selector :has()

:has(), el «selector padre»

Selecciona un elemento según lo que contiene o lo que tiene a continuación.

Hasta ahora el CSS solo podía bajar: de un padre a sus hijos. Con :has() podemos mirar hacia dentro y cambiar al padre en función de sus hijos.

Es uno de los selectores más potentes de los últimos años; está soportado en todos los navegadores actuales.

04 · El selector :has()

Ejemplos de :has()

Tres patrones habituales.

style.css
/* Una figura que contiene un figcaption */
figure:has(figcaption) {
  border: 1px solid #d8e0e9;
}

/* Un enlace que contiene directamente una imagen */
a:has(> img) {
  display: inline-block;
}

/* Un li justo ANTES de otro li con hover (el "hermano anterior") */
li:has(+ li:hover) {
  color: crimson;
}
04 · El selector :has()

:has() y el hover de la clase 15

Por fin podemos hacer hover sobre un hijo y cambiar a sus «hermanos».

En la clase 15 vimos que el hover solo podía afectar a elementos posteriores. Combinando :has() con :not() el contenedor reacciona al hover de cualquier hijo y atenúa a los demás.

style.css
.galeria:has(.item:hover) .item:not(:hover) {
  opacity: 0.4;
}
04 · El selector :has()

:has() en funcionamiento

Las tarjetas con imagen cambian de color y, al pasar el ratón por una, las demás se atenúan.

index.html
<section class="galeria">
  <article class="item">
    <img src="../dinamicos/img1.svg" alt="">
    <h3>Con imagen</h3>
  </article>
  <article class="item">
    <h3>Sin imagen</h3>
    <p>Solo texto.</p>
  </article>
  <article class="item">
    <img src="../dinamicos/img2.svg" alt="">
    <h3>Con imagen</h3>
  </article>
  <article class="item">
    <h3>Sin imagen</h3>
    <p>Solo texto.</p>
  </article>
</section>
style.css
.galeria {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 14px;
}
.item {
  padding: 12px;
  background-color: #f6f8fa;
  border: 1px solid #d8e0e9;
  border-radius: 12px;
  transition: 0.3s;
}
.item img {
  width: 100%;
  height: 80px;
  object-fit: cover;
  border-radius: 8px;
}
.item h3 {
  margin: 8px 0 0;
  font-size: 1rem;
}
.item p {
  margin: 6px 0 0;
}

/* :has() → la tarjeta cambia según lo que contiene */
.item:has(img) {
  background-color: #e6f5d6;
  border-color: #92d050;
}

/* :has() + :not() → el contenedor reacciona al hover de un hijo */
.galeria:has(.item:hover) .item:not(:hover) {
  opacity: 0.4;
}
Resultado: :has()