@starting-style: declarar el estado inicial
Las dos sintaxis, cuándo se aplica exactamente, por qué el orden en la hoja de estilos decide, y los errores que produce colocarla mal.
@starting-style resuelve el problema del estado anterior con una idea de una sola línea: te deja escribir el valor que un elemento tiene antes de empezar a renderizarse. No es un estado más que se pueda seleccionar, no es una pseudo-clase y no ocurre en ningún momento visible: es un cálculo de estilo adicional que el motor hace una única vez, exclusivamente para tener un punto de partida desde el que transicionar. Entender que es un cálculo aparte y no un estado explica todo su comportamiento, incluido el detalle del orden que hace fallar la mitad de los primeros intentos.
- Escribir
@starting-styleen sus dos sintaxis y elegir la adecuada. - Enunciar las condiciones exactas en que el estilo inicial se usa.
- Explicar por qué el orden de la regla en la hoja de estilos cambia el resultado.
- Diagnosticar los tres errores de colocación más frecuentes.
Las dos sintaxis
Anidada dentro de la regla. Es la forma preferible porque mantiene juntos el estado final y el inicial, y porque hereda el selector de la regla que la contiene:
.aviso {
opacity: 1;
translate: 0 0;
transition: opacity 240ms, translate 240ms cubic-bezier(0.2, 0, 0, 1);
@starting-style {
opacity: 0;
translate: 0 -8px;
}
}
Como regla independiente, con su propio selector dentro:
@starting-style {
.aviso {
opacity: 0;
translate: 0 -8px;
}
}
Las dos hacen lo mismo. La anidada exige anidamiento nativo de CSS, que está disponible en los cuatro motores desde hace tiempo. La independiente es útil cuando quieres agrupar los estilos iniciales de varios componentes o cuando no puedes anidar.
Cuándo se usa exactamente
El estilo inicial se aplica solo cuando el elemento no tiene un estilo anterior al cambio, es decir, en las tres situaciones que vimos en la lección anterior: cuando el elemento empieza a renderizarse por primera vez, cuando vuelve de display: none, y cuando entra en la capa superior.
De ahí salen tres consecuencias que conviene tener claras.
No se aplica en cambios normales. Si el elemento ya se está renderizando y cambias una clase, @starting-style se ignora por completo. No es un mecanismo general para “el estado anterior de cualquier transición”: es específicamente para el estado anterior de un elemento que no lo tenía.
No se aplica a la salida. Cuando el elemento desaparece, sí hay estilo anterior —el que tenía mientras se veía—, así que el motor no necesita ninguna ayuda. La salida se resuelve con una transición normal, y el único problema pendiente es que el elemento siga renderizándose mientras dura, que es tema de las lecciones siguientes.
No es visible nunca. El estilo inicial existe durante cero tiempo: es el valor de partida de la interpolación, no un estado por el que el elemento pase. Poner en @starting-style un display: none o un visibility: hidden no oculta nada; solo declara un valor de partida para una interpolación que empieza inmediatamente.
Dónde se coloca la regla, y por qué decide
El orden en la hoja de estilos
Aquí está el detalle que hace fallar la mitad de los primeros intentos y que no es evidente: la regla @starting-style y la regla original tienen la misma especificidad. Cuando dos declaraciones tienen la misma especificidad, gana la última en orden de aparición. Por tanto, si escribes @starting-style antes de la regla que define el estado final, la regla final gana también en el cálculo del estilo inicial, y el estilo inicial acaba siendo idéntico al final. Sin diferencia no hay transición.
/* Roto: la regla normal viene despues y gana. */
@starting-style {
.aviso { opacity: 0; }
}
.aviso {
opacity: 1;
transition: opacity 240ms;
}
/* Correcto: el estilo inicial viene despues. */
.aviso {
opacity: 1;
transition: opacity 240ms;
}
@starting-style {
.aviso { opacity: 0; }
}
La forma anidada elimina este problema por construcción, porque el bloque @starting-style está dentro de la regla y por tanto siempre después de sus propias declaraciones. Es la principal razón práctica para preferirla.
El mismo cuidado hay que tener con las capas de la cascada: si tus estilos base están en una capa de mayor prioridad que la que contiene el @starting-style, ganarán igualmente. La regla es que el estilo inicial tiene que poder vencer al estilo normal dentro del cálculo del estilo inicial, y todo lo que valga para la cascada normal vale aquí.
Hay un modelo mental erróneo que produce comportamientos desconcertantes y que cuesta descartar porque el nombre de la regla lo sugiere: pensar que @starting-style define el estado inicial completo del elemento, como si fuera un fotograma from de una animación. No lo es. El motor calcula el estilo inicial tomando el estilo normal del elemento —el que tendrá cuando exista, con toda su cascada resuelta— y aplicando encima las declaraciones que hayas puesto en @starting-style. Todo lo que no menciones ahí vale lo mismo que en el estado final. Esa diferencia tiene dos consecuencias prácticas de signo opuesto. La buena: no tienes que repetir nada. Si el elemento tiene veinte propiedades y solo dos cambian al aparecer, escribes esas dos y el resto se hereda del estado final sin que se produzca ninguna transición sobre ellas, porque sus dos valores son iguales. Comparado con un @keyframes, donde el fotograma from compite con la cascada y hay que ser explícito, esto es notablemente más limpio. La mala, y la que produce el bug: si el estado final depende de una clase o un atributo que también acaba de aparecer, el estilo inicial ya lo incluye. Es el caso de un panel que se abre con [data-abierto] y cuyo @starting-style está dentro de esa misma regla: el estilo inicial se calcula con el atributo puesto, así que hereda todo lo que ese atributo aporta salvo lo que declares explícitamente. Casi siempre es lo que quieres, pero cuando no lo es —porque el atributo cambia también el tamaño, o el color, o la posición— acabas con transiciones que no esperabas o con ausencias de transición que no te explicas, y la causa está en propiedades que ni siquiera aparecen en tu @starting-style.
Los tres errores de colocación
Error uno: poner @starting-style en la regla base en lugar de en la regla del estado abierto. Si el elemento aparece porque se le añade un atributo, el estilo inicial tiene que estar donde está el estado final.
/* Roto: el estilo inicial se declara para el estado cerrado. */
.panel {
opacity: 0;
transition: opacity 240ms;
@starting-style { opacity: 0; }
}
.panel[data-abierto] { opacity: 1; }
/* Correcto: el estilo inicial acompana al estado final. */
.panel {
opacity: 0;
transition: opacity 240ms;
}
.panel[data-abierto] {
opacity: 1;
@starting-style { opacity: 0; }
}
Error dos: olvidar que la transición tiene que estar declarada en el estado de destino. El estilo inicial da el punto de partida, pero quien decide si se interpola es la propiedad transition del estilo posterior al cambio. Si la transición solo está en la regla base y el estado abierto la reinicia, no hay interpolación.
Error tres: esperar que funcione sin que el elemento se renderice. Si el elemento sigue con display: none en el estado de destino, no hay nada que animar. @starting-style resuelve el punto de partida, no la visibilidad; para eso hace falta la pieza de la lección siguiente.
Un ejemplo completo que funciona hoy
Un aviso que se inserta en el DOM y aparece animado, sin una línea de JavaScript de animación:
.aviso {
position: fixed;
inset-block-end: 1rem;
inset-inline-end: 1rem;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
background: oklch(96% 0.02 250);
color: #11111b;
opacity: 1;
translate: 0 0;
scale: 1;
transition:
opacity 200ms linear,
translate 280ms cubic-bezier(0.2, 0, 0, 1),
scale 280ms cubic-bezier(0.2, 0, 0, 1);
@starting-style {
opacity: 0;
translate: 0 12px;
scale: 0.96;
}
}
@media (prefers-reduced-motion: reduce) {
.aviso {
transition: opacity 120ms linear;
@starting-style { opacity: 0; translate: 0 0; scale: 1; }
}
}
// El JavaScript solo inserta. La animacion es del CSS.
function avisar(texto) {
const el = document.createElement('div');
el.className = 'aviso';
el.setAttribute('role', 'status');
el.textContent = texto;
document.body.append(el);
}
Fíjate en el bloque de movimiento reducido: la variante sin desplazamiento se declara con su propio @starting-style, porque los valores iniciales de translate y scale tienen que igualar a los finales para que no haya movimiento. Declarar solo la transición no bastaría: el estilo inicial seguiría diciendo que el elemento empieza doce píxeles más abajo, y saltaría.
Soporte: @starting-style está en los cuatro motores desde agosto de 2024 —Chrome y Edge 117, Safari 17.5, Firefox 129—. Su modo de fallo es benigno: un motor que no la entienda ignora la regla y el elemento aparece de golpe.
- Escribe un componente que aparezca con desvanecido y desplazamiento usando la sintaxis anidada.
- Reescríbelo con la sintaxis independiente y colócala antes de la regla original. Comprueba que no funciona y explica por qué.
- Muévela después y comprueba que sí.
- Añade un estado con atributo y coloca el
@starting-styleprimero en la regla base y luego en la del atributo. Anota qué cambia.