Crear animaciones avanzadas en CSS con @property

Aprende a animar variables CSS con @property: crea un anillo de progreso y un contador de pasos sin JavaScript. Incluye el código de la tarjeta.

Animar variables CSS con @property permite pasar de un valor a otro de manera gradual. En este tutorial lo vas a usar para llenar un anillo de progreso y hacer que un contador de pasos suba desde cero, sin escribir JavaScript para esas dos animaciones.

En este tutorial

Qué vas a construir

La tarjeta muestra un número de pasos y un anillo que se llena alrededor de un icono. El panel final añade otras métricas y un selector de tema. En el video nos concentramos en las dos animaciones: el anillo y la cifra que cuenta hacia arriba. El modo claro y oscuro llega en un próximo tutorial.

Tarjeta de pasos con un anillo de progreso animado con @property

Qué necesitas

Paso a paso

Empieza con una custom property normal

Una variable CSS comienza con dos guiones y se consulta mediante var(). El proyecto usa este patrón para reutilizar colores como --color-coral. Si cambias el valor donde se declara, los lugares que usan var(--color-coral) se actualizan juntos. Ese es el uso que probablemente ya conoces. El problema aparece cuando intentas pasar suavemente entre dos valores de una variable sin registrar: el navegador no dispone de un tipo declarado para interpolarlos. En el video primero se muestra el salto brusco del progreso para que la diferencia sea visible cuando añadamos @property.

:root {
  --color-coral: #8a15ea;
}

.ring {
  background-image: conic-gradient(
    var(--color-coral) var(--progreso),
    var(--color-superficie) 0
  );
}

El nombre conserva «coral» aunque el valor del archivo de la demostración se cambió a morado durante el video.

Dibuja el anillo con conic-gradient()

conic-gradient() reparte colores alrededor de un punto central. En la tarjeta, el primer color llega hasta var(--progreso); a partir de ahí se pinta el color de la superficie. Con border-radius: 50% la caja se ve circular. Un elemento interior, también redondo, tapa el centro del degradado y deja visible solo el borde coloreado. No hace falta una imagen para cada porcentaje: el mismo CSS puede dibujar 0 %, 30 % u 80 % al cambiar la propiedad. En el ejemplo del video el anillo tiene 104 píxeles de ancho.

.ring {
  display: grid;
  place-items: center;
  flex: none;
  width: 104px;
  aspect-ratio: 1;
  background-image: conic-gradient(
    var(--color-coral) var(--progreso),
    var(--color-superficie) 0
  );
  border: 2px solid var(--color-borde);
  border-radius: 50%;
}

Registra el porcentaje con @property

Ahora sí viene la pieza que cambia el comportamiento. @property registra --progreso y le dice al navegador que su valor es un <percentage>. También define si se hereda y cuál es el valor inicial. En la demostración se usa inherits: true y initial-value: 0%; por eso no necesitamos escribir el cero otra vez en el anillo. En un componente aislado también podrías elegir inherits: false para que cada anillo tenga su propio valor inicial. Lo esencial para esta animación es registrar el tipo porcentual y conservar valores válidos al cambiarlo.

@property --progreso {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 0%;
}

Cuidado

syntax e inherits son obligatorios. Si la sintaxis no es "*", también necesitas un initial-value válido e independiente. Si omites uno, la regla de registro puede ignorarse.

Anima el progreso del anillo

El código del componente lleva --progreso del valor inicial a 80% durante 1,5 segundos. animation-fill-mode: forwards mantiene el estado final una vez termina la animación. La regla from se puede omitir aquí porque el registro ya establece 0% como valor inicial. Esta es la parte que antes daba un salto de golpe: al conocer el tipo porcentual, el navegador puede calcular los valores intermedios y el degradado parece llenarse. Prueba a cambiar 80% por otro porcentaje y verás que el anillo sigue el mismo recorrido. Si además quieres animar la entrada de la tarjeta, mira 3 características modernas de CSS.

.ring {
  animation: mostrar-circulo 1.5s forwards;
}

@keyframes mostrar-circulo {
  to {
    --progreso: 80%;
  }
}

Patrocinador oficial del curso

Publica tus proyectos con Hostinger

En el video se ve cómo subir un ZIP del sitio desde el panel de Hostinger. Si quieres publicar tus proyectos con tu dominio, puedes conocer sus planes desde el enlace de AlexCG Design.

Usa un contador CSS para mostrar pasos

El número de pasos no necesita escribirse dentro del pseudo-elemento. counter-reset crea o reinicia un contador llamado pasos, y content: counter(pasos) muestra su valor. Durante la explicación del video también se prueba counter-increment con elementos internos. Ahí aparece un detalle útil: ::before se genera antes de esos elementos y no refleja los incrementos posteriores; ::after sí aparece después. Para el contador final simplificamos el ejemplo: el valor viene directamente de una custom property entera, así que no hacen falta elementos extra para sumar uno por uno.

.card__count {
  counter-reset: pasos var(--contador);
}

.card__count::after {
  content: counter(pasos);
}

Registra el entero y anima la cifra

La segunda propiedad registrada acepta un <integer> y empieza en cero. El video la lleva a 8680 para demostrar que el número visible puede animarse solo con CSS. El pseudo-elemento muestra el contador asociado a --contador, mientras @keyframes cambia esa propiedad durante 1,5 segundos. En el archivo final del panel se generaliza la idea con --valor y --meta: cada tarjeta declara sus cifras, --num es el entero animado y --progreso calcula cuánto debe llenarse el anillo. Así una misma estructura sirve para pasos, agua y minutos activos.

@property --contador {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

.card__count {
  counter-reset: pasos var(--contador);
  animation: incrementar-pasos 1.5s forwards;
}

@keyframes incrementar-pasos {
  to {
    --contador: 8680;
  }
}

Junta las dos animaciones

Así queda todo el CSS de la tarjeta en un solo bloque: las dos propiedades registradas, el anillo, el contador y los dos @keyframes. Pégalo en tu hoja de estilos y cambia 80% y 8680 para probar otros valores.

@property --progreso {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 0%;
}

@property --contador {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

.ring {
  display: grid;
  place-items: center;
  flex: none;
  width: 104px;
  aspect-ratio: 1;
  background-image: conic-gradient(
    var(--color-coral) var(--progreso),
    var(--color-superficie) 0
  );
  border: 2px solid var(--color-borde);
  border-radius: 50%;
  animation: mostrar-circulo 1.5s forwards;
}

.card__count {
  counter-reset: pasos var(--contador);
  animation: incrementar-pasos 1.5s forwards;
}

.card__count::after {
  content: counter(pasos);
}

@keyframes mostrar-circulo {
  to {
    --progreso: 80%;
  }
}

@keyframes incrementar-pasos {
  to {
    --contador: 8680;
  }
}

Consejo

Deja el número final también en el HTML como texto accesible para lectores de pantalla. El panel completo usa un valor visible generado por CSS y un span de solo lectura visual con la cifra real.

Código fuente

El repositorio de GitHub contiene la tarjeta de pasos que construimos en el video: index.html, estilos.css y sus recursos. Puedes descargarlo desde Code → Download ZIP. El panel Ritmo completo aparece al final del video, pero no forma parte de este repositorio.

Código de la tarjeta de pasos: HTML, CSS y recursos del ejemplo.

Preguntas frecuentes

¿Por qué mi variable CSS no se anima con transition?

Una custom property declarada solo con --nombre no tiene un tipo de valor registrado. Por eso un cambio de 0% a 80% suele comportarse como un salto discreto en este caso. Regístrala con @property, usa syntax: "<percentage>" y un valor inicial válido; luego aplica la transición o animación a esa propiedad. Comprueba también que los dos valores tengan el mismo tipo.

¿Qué hace inherits dentro de @property?

Controla si los hijos reciben por defecto el valor registrado del elemento padre. Con true, un hijo puede usar el valor que se estableció arriba en el árbol; con false, usa su propio valor o el initial-value del registro. No decide si la propiedad se puede animar: eso depende del tipo declarado y de los valores entre los que intentas pasar.

¿Necesito JavaScript para animar el anillo y el contador?

No para las dos animaciones que muestra este video. conic-gradient() dibuja el anillo, @property registra el porcentaje y el entero, y @keyframes cambia esos valores. El contador CSS imprime la cifra. JavaScript sí puede ser útil si los datos cambian por una acción real del usuario o llegan de un servicio; el ejemplo grabado usa valores definidos en el proyecto.

Más proyectos

Curso

Sigue practicando CSS

En el curso completo de CSS encontrarás más proyectos para aplicar estas ideas a tus interfaces.

Deja un comentario