3 características que no conocías de CSS: @starting-style, transition-behavior y nesting

💻 Descarga el código del proyecto
El buscador con popover completo (HTML y CSS) listo para abrir y probar.
Descargar el código en GitHub →

Es hora de cambiar como se escribe CSS.

CSS ha cambiado bastante en estos años, es algo que me alegra muchisimo. Cosas que antes necesitaban JavaScript, trucos con visibility o un preprocesador como Sass hoy se resuelven con una o dos líneas de CSS nativo.

El problema es que estas novedades no se hacen tan populares, es como tener al amor de tu vida a 2 cuadras de tu casa y no verla:(, esto hace que mucha gente siga escribiendo CSS como hace 5 años. En este artículo veremos 3 características que ya puedes empezar a usar en tu día a día: @starting-style, transition-behavior y el nesting.  (Me lo agradecerás)

Antes y hoy

Lo que queremos hacerAntesHoy
Animar un elemento que acaba de aparecerJavaScript: añadir una clase un instante después@starting-style
Animar un display: none a visible (y al revés)JavaScript con transitionend, o trucos con visibilitytransition-behavior: allow-discrete
Escribir selectores sin repetirlosSass o Less, con un paso de compilaciónnesting nativo

En pocas palabras: cada una de estas características elimina una dependencia, ya sea JavaScript para animar o un preprocesador para organizar.

¿Por qué usarlas desde hoy?

  • Menos JavaScript: las animaciones de entrada y salida pasan a ser solo CSS.
  • Menos herramientas: no necesitas compilar Sass solo para anidar selectores.
  • Ya funcionan en las versiones actuales de Chrome, Safari y Firefox.
  • Las dos de animación degradan bien: si un navegador no las entiende, el elemento simplemente aparece sin animación y sigue funcionando.
💡 Con el nesting es distinto: un navegador que no lo entienda ignora esas reglas anidadas. Antes de usarlo revisa el soporte en MDN o en caniuse, según los navegadores que debas cubrir.

¿Qué necesito para seguir este artículo?

Solo necesitas saber lo básico de transition (propiedad, duración) y haber visto alguna vez el atributo popover. Si nunca has usado transiciones en CSS, ve primero a ese tema y regresa.

Nuestro banco de pruebas

Para ver las 3 características en acción usaremos un buscador con popover, una ventana flotante de HTML que no necesita JavaScript. Pero quiero que te fijes en las propiedades, no en el componente: lo mismo que veremos aquí lo puedes aplicar a modales, menús, tooltips y notificaciones.

Archivos para este proyecto

Crear una carpeta llamada paleta y dentro de ella dos archivos: index.html y estilos.css.

Si prefieres no escribirlo todo, descarga el código completo desde GitHub.

El HTML de nuestra paleta

Vamos a necesitar un botón que abra la paleta y un contenedor con el atributo popover. El atributo popovertarget conecta el botón con el contenedor por su id, sin JavaScript.

<!-- index.html -->
<link rel="stylesheet" href="estilos.css">

<button popovertarget="paleta">Abrir paleta de comandos</button>

<div id="paleta" class="palette" popover>
	<input type="text" placeholder="Busca un comando...">
	<ul>
		<li>Crear carpeta</li>
		<li>Abrir proyecto</li>
		<li>Cambiar tema</li>
	</ul>
</div>
🧠 Momento de pasar al código CSS. (Manos a la obra)

1. @starting-style: animar lo que acaba de aparecer

Cómo se hacía antes.

Para animar un elemento que acababa de aparecer teníamos que usar JavaScript: añadir el elemento al DOM, esperar un instante (con requestAnimationFrame o un setTimeout) y recién ahí agregar una clase que disparara la transición. Funcionaba, pero es código frágil para algo que es puramente visual.

El problema que resuelve @starting-style.

Analogía:

Podríamos imaginar una transición como una libreta de dibujos donde pasas las hojas rápido: para que haya movimiento necesitas dos dibujos, el de antes y el de después. Un elemento que acaba de aparecer solo tiene el dibujo de después; le falta la primera hoja.

Una transición necesita un estado anterior desde el cual animar. Cuando un elemento ya estaba en la página, el navegador lo tiene. Pero cuando pasa de display: none a visible, como nuestro popover, el navegador no tiene ese «antes» y simplemente muestra el resultado final.

Problema.

Tenemos un popover con una transición de opacity y transform de .3s. Al abrirlo, ¿se animará su entrada?

/* estilos.css */
.palette{
	opacity: 0;
	transform: translate(-12px) scale(.8);
	transition-property: opacity, transform;
	transition-duration: .3s;
}

.palette:popover-open{
	opacity: 1;
	transform: unset;
}

Respuesta:

Lamentablemente no. Si te das cuenta en mi navegador, la paleta aparece de golpe, aunque la transición esté bien escrita. Esto es porque al abrirse, el elemento no tenía un estado anterior desde el cual animar.

Para eso existe @starting-style, una regla donde definimos cómo se ve el elemento justo en el primer momento en que aparece. Con eso el navegador ya tiene la primera hoja de la libreta.

/* estilos.css */
.palette:popover-open{
	opacity: 1;
	transform: unset;
}

@starting-style{
	.palette:popover-open{
		opacity: 0;
		transform: translate(-12px) scale(.8);
	}
}

Ahora sí nos damos cuenta de que la paleta entra deslizándose y creciendo. Pero si la cerramos, desaparece de golpe otra vez. De eso se encarga la siguiente característica.

Consideraciones:

  • @starting-style solo afecta al primer momento en que el elemento se muestra; después el elemento usa sus estilos normales.
  • Los valores que pongas ahí deben ser propiedades que tengan una transition, de lo contrario no habrá nada que animar.
  • También funciona con <dialog> y con elementos que se añaden al DOM con JavaScript.

En tu día a día.

Úsala cada vez que algo aparezca en pantalla y quieras que lo haga con suavidad:

  • Notificaciones o toasts que se insertan con JavaScript.
  • Modales con <dialog> y menús desplegables.
  • Tarjetas que se añaden a una lista.

2. transition-behavior: animar la salida

Cómo se hacía antes.

Para animar la salida teníamos dos opciones: combinar opacity con visibility (y dejar el elemento ocupando espacio), o escuchar el evento transitionend con JavaScript para poner display: none cuando la animación terminaba.

El problema que resuelve transition-behavior.

Analogía:

Podríamos imaginar opacity como un regulador de luz (puede estar en 0, en 0.5, en 1) y display como un interruptor (solo está encendido o apagado, sin puntos intermedios). Al cerrar, el interruptor se apaga al instante y nunca llegamos a ver cómo baja la luz.

Cuando cerramos el popover, el navegador aplica display: none inmediatamente. El elemento desaparece antes de que opacity y transform tengan tiempo de animarse. Esto pasa porque display es una propiedad discreta: sus valores (none, block) no tienen valores intermedios, y por defecto las transiciones ignoran a las propiedades discretas.

Problema.

Ya tenemos la transición en opacity y transform, ¿por qué la salida sigue sin animarse?

Respuesta:

Porque display no forma parte de la transición. Debemos hacer dos cosas: agregar display a transition-property, y activar transition-behavior: allow-discrete.

/* estilos.css */
.palette{
	opacity: 0;
	transform: translate(-12px) scale(.8);
	transition-property: opacity, transform, display;
	transition-duration: .3s;
	transition-behavior: allow-discrete;
}

Con allow-discrete le decimos al navegador que espere a que terminen las demás transiciones antes de cambiar el valor discreto: al abrir, display cambia al inicio (para que se vea la animación), y al cerrar, cambia al final (cuando ya terminó de desvanecerse).

En pocas palabras: allow-discrete hace que el interruptor espere a que el regulador de luz termine de bajar.

¡Y listo! Nuestra paleta ya entra y sale con animación, sin JavaScript.

normal vs allow-discrete

transition-behavior solo tiene dos valores y la diferencia práctica es esta:

ValorQué haceCuándo usarlo
normalIgnora las propiedades discretas como display. Es el valor por defecto.Transiciones comunes (opacity, transform, color).
allow-discretePermite transicionar propiedades discretas, cambiando su valor al inicio o al final.Mostrar y ocultar elementos con display (popovers, modales, menús).

Shorthand transition.

Si prefieres escribirlo en una sola línea, allow-discrete va dentro del shorthand, al final de cada propiedad que lo necesite:

/* estilos.css */
.palette{
	transition: opacity .3s, transform .3s, display .3s allow-discrete;
}

Consideraciones:

  • Si te falta display en transition-property, allow-discrete no tiene efecto: el display tiene que estar en la lista.
  • Si en la salida el popover pierde su posición sobre los demás elementos, agrega también overlay a transition-property (es otra propiedad discreta).
  • El ::backdrop de nuestro ejemplo no se anima: aparece y desaparece al instante. Si quieres animarlo necesita su propia transition y su propio @starting-style.

En tu día a día.

Úsala cada vez que algo desaparezca con display: none y quieras que se vaya con suavidad:

  • Menús móviles y paneles que se abren y se cierran.
  • Modales con <dialog> y popovers.
  • Cualquier elemento que ocultes con display: none desde una clase.

3. nesting: escribir CSS dentro de CSS

Cómo se hacía antes.

Anidar selectores era una de las razones principales para instalar Sass o Less: necesitábamos un paso de compilación solo para no repetir el nombre de la clase en cada regla.

El problema que resuelve el nesting.

Analogía:

Podríamos imaginar la dirección de una casa: en lugar de decir «Calle Principal, casa 5, cocina» y «Calle Principal, casa 5, sala», entramos a la casa y ahí decimos solo «cocina» y «sala».

Fíjate en cómo quedó nuestro CSS hasta ahora. Escribimos .palette en cada regla, y el @starting-style quedó separado de .palette:popover-open, aunque va dedicado a él:

/* estilos.css */
.palette{
	opacity: 0;
	transform: translate(-12px) scale(.8);
	transition-property: opacity, transform, display;
	transition-duration: .3s;
	transition-behavior: allow-discrete;
}

.palette::backdrop{
	background-color: rgba(8, 8, 8, 0.288);
	filter: blur(12px);
}

.palette:popover-open{
	opacity: 1;
	transform: unset;
}

@starting-style{
	.palette:popover-open{
		opacity: 0;
		transform: translate(-12px) scale(.8);
	}
}

Con el nesting podemos escribir las reglas hijas dentro de la regla padre, y el navegador se encarga de unirlas. Esto lo hacía antes Sass, pero ahora CSS lo hace de forma nativa, sin compilar nada.

Error deliberado: olvidar el &.

Empecemos anidando :popover-open dentro de .palette:

/* estilos.css */
.palette{
	opacity: 0;
	transform: translate(-12px) scale(.8);
	transition-property: opacity, transform, display;
	transition-duration: .3s;
	transition-behavior: allow-discrete;

	:popover-open{
		opacity: 1;
		transform: unset;
	}
}

Lamentablemente la paleta se abre, pero no la vemos. Esto es porque, sin el símbolo &, el navegador interpreta :popover-open como un descendiente de .palette (.palette :popover-open), no como la propia .palette. Para que se aplique al mismo elemento debemos poner & delante.

El símbolo &.

El & representa al selector padre. Es necesario con pseudoclases (&:popover-open, &:hover) y pseudoelementos (&::backdrop), y para unir clases al mismo elemento (&.activo).

Y ahora sí, este es el código completo, con las 3 características juntas:

/* estilos.css */
.palette{
	opacity: 0;
	transform: translate(-12px) scale(.8);
	transition-property: opacity, transform, display;
	transition-duration: .3s;
	transition-behavior: allow-discrete;

	&::backdrop{
		background-color: rgba(8, 8, 8, 0.288);
		filter: blur(12px);
	}

	&:popover-open{
		opacity: 1;
		transform: unset;

		@starting-style{
			opacity: 0;
			transform: translate(-12px) scale(.8);
		}
	}
}

Quiero que veas que el @starting-style ahora está dentro de &:popover-open: ya no repetimos el selector, y todo lo de .palette vive en un solo bloque. Cada regla anidada es la misma que teníamos arriba, pero mucho más ordenada.

🧠 También podemos anidar @media, @container y @supports dentro de una regla, para tener cada estilo junto a su responsive.

Consideraciones:

  • Evita anidar demasiado: 2 o 3 niveles suelen ser suficientes para que el código siga siendo legible.
  • Al anidar .hijo o a como descendientes, el & no es obligatorio; con pseudoclases y pseudoelementos sí.

En tu día a día.

El nesting no es solo para popovers: lo puedes aplicar a cualquier componente que ya tengas. Por ejemplo, en una tarjeta, sus títulos, su hover y su responsive viven en un solo bloque:

/* estilos.css */
.tarjeta{
	padding: 1rem;
	border-radius: 12px;

	h2{
		font-size: 1.25rem;
	}

	&:hover{
		box-shadow: 0 8px 24px rgb(0 0 0 / .2);
	}

	@media (width < 600px){
		padding: .75rem;
	}
}

Resumen de las 3 características

CaracterísticaAntesHoyÚsala en
@starting-styleJavaScript para añadir una clase después de que aparece el elemento@starting-style{ ... } con el estilo inicialNotificaciones, modales, menús, elementos añadidos con JavaScript
transition-behaviorvisibility o transitionend con JavaScripttransition-behavior: allow-discrete; y display en transition-propertyMenús móviles, paneles, <dialog>, popovers
nestingSass o Less con compilaciónReglas hijas dentro del padre, con & cuando haga faltaCualquier componente de tu proyecto

En pocas palabras: @starting-style anima la entrada, transition-behavior anima la salida y el nesting mantiene todo el código ordenado.

¡Felicidades! Has creado una paleta de comandos que entra y sale con animación, sin JavaScript.

Por dónde empezar.

Para mí la que más rápido se nota en el día a día es el nesting, porque la puedes aplicar hoy mismo a un archivo CSS que ya tengas. Las otras dos aparecen en cuanto tengas un elemento que entra y sale de la pantalla.

Ejercicios para empezar hoy:

  • Toma el CSS de un componente tuyo y anida sus reglas con &.
  • Agrega @starting-style a un elemento que se muestre con JavaScript.
  • Anima la salida de un <dialog> o de un popover con allow-discrete.

Descarga el código y mira el video

¿Quieres ver las 3 características en vivo, con el código funcionando en el navegador? Mira el video completo «3 características que no conocías de CSS» en mi canal de YouTube.

Y si prefieres probarlo directamente, descarga el código completo del buscador desde GitHub para pegarlo en tu proyecto.

Deja un comentario