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.
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).
: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.
<ul class="lista">
<li>Manzana</li>
<li class="agotado">Pera</li>
<li>Uva</li>
</ul>
.lista li:not(.agotado) {
background-color: #e6f5d6;
}
:is() y :where()
Agrupan varios selectores en uno.
:is() evita repetir el prefijo en cada selector de una lista.
/* 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).
:where(.texto) span {
color: crimson;
}
: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.
.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):
.lista li:empty::before {
content: "(vacío)";
color: #999;
}
Selectores en funcionamiento
Código completo y resultado.
<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>
.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;
}
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.
.card h3 {
color: #364f6b;
}
.card p {
color: #555;
}
.card:hover {
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
}
.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.
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.
.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;
}
}
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.
.card {
padding: 20px;
@media (max-width: 500px) {
padding: 12px;
}
}
Nesting en funcionamiento
Dos tarjetas con hover, botón y versión destacada, todo anidado en .card.
<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>
.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;
}
}
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.
/* 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).
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.
.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.
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().
.tarjeta h3 {
font-size: clamp(1rem, 5cqw, 1.6rem);
}
Container query en funcionamiento
La misma tarjeta dentro de dos cajas de distinto ancho.
<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>
.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;
}
}
: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.
Ejemplos de :has()
Tres patrones habituales.
/* 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;
}
: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.
.galeria:has(.item:hover) .item:not(:hover) {
opacity: 0.4;
}
:has() en funcionamiento
Las tarjetas con imagen cambian de color y, al pasar el ratón por una, las demás se atenúan.
<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>
.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;
}