Contadores regresivos sin desvíos en React: de setTimeout a CSS puro
Compare setTimeout, requestAnimationFrame y una técnica CSS sin JavaScript para contadores regresivos en React, incluyendo el truco del retraso único que mantiene los dígitos sincronizados.
Una tarjeta de cupón con fecha de vencimiento necesita un reloj visible: horas, minutos y segundos que cuenten hacia abajo hasta que la oferta desaparezca. Este elemento es diminuto, pero tiene una gran influencia en la percepción del producto. Un temporizador que se retrasa con respecto al tiempo real, salta dos segundos de golpe o mantiene el ventilador de una computadora portátil girando en silencio socava la confianza del usuario en todo lo que lo rodea. Esta guía explica tres formas de crear ese contador regresivo en React, detalla exactamente por qué el primer método presenta desviaciones y finaliza con una técnica basada únicamente en CSS que permite gestionar cientos de temporizadores sin necesidad de código JavaScript para cada actualización.
El problema en una frase
Cada cupón cuenta con un claim_end_time. La pantalla debe mostrar cuánto tiempo queda hasta ese momento, formateado como HH:MM:SS, actualizándose cada segundo, y debe detenerse en cero cuando el cupón venza.
La aritmética es trivial. La parte difícil es decidir cómo y cuándo volver a calcularla, y esa elección determina la precisión, el costo en CPU y qué se puede hacer cuando se agota el tiempo.
Enfoque 1: una cadena de setTimeout, y por qué se desvía
La implementación instintiva es una función que actualiza la pantalla y luego se programa a sí misma nuevamente después de un segundo.
function tick() {
updateDisplay();
setTimeout(tick, 1000);
}
tick();
Se lee de manera clara y parece funcionar en pruebas rápidas. El problema radica en cómo se entregan los temporizadores. JavaScript en la página se ejecuta en un único hilo principal, y el bucle de eventos le pasa una tarea a la vez. Una función de callback de temporizador es simplemente otra tarea, colocada en cola detrás de los manejadores de clics, las callbacks de red, el trabajo de renderizado y todo lo que React está reconciliando. El valor 1000 que se pasa es un mínimo, no una promesa. Si el hilo está ocupado cuando transcurre un segundo, la función de callback espera, y es posible que se ejecute después de 1,050 ms o 1,200 ms en lugar de eso. Si desea recordar por qué el hilo principal se comporta de esta manera, consulte nuestro artículo sobre el hilo único de JavaScript frente al motor multiproceso del navegador.
Lo que convierte la demora ocasional en un retraso acumulativo es el encadenamiento de los eventos. Cada tick programa el siguiente tick desde su propio interior, por lo que cualquier demora se transmite a todos los ticks posteriores. Nada hace que la secuencia vuelva al ritmo del reloj de pared. Deja que tal bucle funcione durante diez minutos, compáralo con Date.now(), y verás que queda atrás.
Un temporizador que disminuye un contador en cada tick mide las llamadas de callback, no el tiempo transcurrido, y la limitación en las pestañas en segundo plano hace que la diferencia sea aún mayor.
Puedes compensar midiendo cuán tarde se ejecuta cada callback y acortando en consecuencia el siguiente retraso. Eso funciona, pero al final debes mantener una lógica de corrección que compite con la plataforma en lugar de aprovecharla. De cualquier manera, el código sigue ejecutándose en el hilo principal cada segundo.
Enfoque 2: requestAnimationFrame y recálculo a partir del reloj
requestAnimationFrame (rAF) pide al navegador que ejecute su función justo antes de la siguiente actualización de la pantalla, lo cual en la mayoría de las pantallas ocurre aproximadamente 60 veces por segundo. Sin embargo, la frecuencia no es lo importante. El cambio relevante es conceptual: deja de preguntar “¿ha pasado otro segundo?” y en su lugar preguntas “¿cuánto tiempo queda en este momento exacto?”
function updateTime() {
const now = Date.now();
const remaining = endTime - now;
if (remaining <= 0) {
display('00:00:00');
return;
}
display(format(remaining));
requestAnimationFrame(updateTime);
}
requestAnimationFrame(updateTime);
En cada frame, el tiempo restante se calcula desde cero, utilizando Date.now() como única fuente de verdad. No hay contador de ticks ni intervalos acumulados. Si un frame llega tarde, el siguiente simplemente vuelve a leer la hora y muestra el valor correcto. Los errores no pueden acumularse porque nada se transfiere de un frame al siguiente aparte del tiempo final en sí. El mismo principio sirve también para la versión con setTimeout: si cada callback vuelve a calcular desde Date.now() en lugar de disminuir un contador, la visualización se corrige automáticamente incluso cuando los callbacks llegan tarde.
Una objeción razonable: el valor visible cambia solo una vez por segundo, ¿entonces por qué realizar cualquier operación 60 veces por segundo? En React, la parte costosa no es la resta sino la vuelta a renderizar provocada por setState. Por lo tanto, el bucle compara en cada frame y actualiza el estado solo cuando realmente cambia el valor correspondiente a un segundo completo.
Actualizar el estado solo cuando cambia el segundo
Aquí está esa idea dentro de un componente. Tenga en cuenta que endTime es una marca de tiempo de Unix en segundos, por lo que se multiplica por 1000 antes de ser comparado con Date.now().
const updateTime = (): void => {
const now = Date.now();
const timeTillStop = endTime * 1000 - now;
if (timeTillStop <= 0) {
setTimeLeft({ hours: 0, minutes: 0, seconds: 0 });
onNoCountdownTimeLeft?.();
return;
}
const totalSeconds = Math.floor(timeTillStop / 1000);
if (totalSeconds !== lastSecondsRef.current) {
lastSecondsRef.current = totalSeconds;
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
setTimeLeft({ hours, minutes, seconds });
}
animationRef.current = requestAnimationFrame(updateTime);
};
Dos referencias se encargan de la tarea principal. lastSecondsRef almacena el segundo más reciente que se ha renderizado. Al ser una referencia, escribir en ella nunca provoca un renderizado; es simplemente un espacio de memoria que persiste entre frames. Por lo tanto, el bucle se ejecuta a la tasa de frames, mientras que React renderiza como máximo una vez por segundo. La función de callback opcional onNoCountdownTimeLeft se llama una vez que se agota el tiempo, lo que permite al componente padre ocultar la tarjeta, volver a obtener datos o mostrar un estado de “vencido”.
La segunda referencia, animationRef, almacena el ID devuelto por requestAnimationFrame. La función de limpieza del efecto la utiliza para cancelar el frame pendiente cuando el componente se desmonta o endTime cambia:
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
Sin esa limpieza, un bucle seguiría ejecutándose después de que el usuario abandone la página, llamando a setState en un componente ya desmontado y desperdiciando recursos del procesador. Con ella, el componente funciona con precisión, su renderizado es económico y se corrige automáticamente.
Unas notas prácticas: los navegadores pausan las llamadas de rAF en pestañas ocultas, lo cual resulta útil aquí: el bucle deja de consumir recursos en segundo plano, y el primer frame después de que la pestaña se vuelva visible lee el valor actual del temporizador y pasa directamente al correcto. La comparación por frame es insignificante para un único temporizador, pero suma importancia al tener decenas de tarjetas.
Enfoque 3: un contador regresivo hecho exclusivamente con CSS
La versión basada en rAF sigue ejecutando JavaScript en cada frame. El tercer enfoque elimina por completo esa necesidad: JavaScript calcula un único número cuando el componente se monta, y a partir de entonces el motor de animaciones del navegador se encarga de cada actualización.
El conteo regresivo se convierte en una animación CSS que recorre los dígitos uno por uno, iniciándose con un retraso negativo para que comience a mitad de la secuencia en el momento adecuado.
Una aclaración sobre el rendimiento antes de continuar. Puede leerse que las animaciones CSS siempre se ejecutan en el hilo del compositor y, por lo tanto, nunca pueden bloquearse. Esto es cierto solo para un pequeño conjunto de propiedades, principalmente transform, opacity y algunos filtros. La animación de propiedades personalizadas registradas y del contenido generado, como lo hace esta técnica, es gestionada por el motor de estilos del navegador en el hilo principal. La verdadera ventaja es diferente pero igualmente significativa: no hay temporizadores, ni estado de React ni renders por tick, y el propio programador del navegador, que relaciona el progreso de la animación con la línea de tiempo del documento, mantiene los valores correctos.
Para que esto funcione, se requiere la colaboración de cuatro características de CSS. Las analizaremos una por una.
Paso 1: declarar enteros animables con @property
Un primer intento obvio es animar directamente la propiedad content. Eso falla, porque content no es interpolable: el navegador no tiene idea de cómo una cadena “59” se convierte en “58”. Lo que se necesita es un número que el navegador pueda animar, y una propiedad personalizada registrada proporciona exactamente eso.
@property --s {
syntax: '<integer>';
inherits: true;
initial-value: 59;
}
@property registra --s con un tipo específico. Al declarar syntax: '<integer>' se indica al navegador que el valor es un número entero real y no un token opaco, lo que permite su interpolación y, por tanto, su animación. Al establecer inherits: true, los elementos descendientes, incluidos los pseudo-elementos ::after, pueden leer el valor animado de su padre. Esto significa que una sola animación en el contenedor del temporizador afecta a cada dígito que contiene. Para conocer más sobre las propiedades registradas y cómo difieren de las propiedades personalizadas comunes, consulte nuestra guía sobre propiedades personalizadas de CSS en tiempo de ejecución y @property.
Cada unidad cuenta con su propia propiedad registrada. Las horas comienzan en 99, que constituye el límite superior que puede mostrar este temporizador.
@property --s {
syntax: '<integer>';
inherits: true;
initial-value: 59;
} /* seconds */
@property --m {
syntax: '<integer>';
inherits: true;
initial-value: 59;
} /* minutes */
@property --h {
syntax: '<integer>';
inherits: true;
initial-value: 99;
} /* hours */
Paso 2: renderizar el número entero como texto con relleno mediante counter()
El número entero aún debe convertirse en texto con relleno de ceros (05, no 5). La función counter() muestra el valor del contador dentro de content, y su estilo integrado decimal-leading-zero proporciona automáticamente el relleno de dos dígitos.
El vínculo entre la propiedad personalizada y el contador es counter-reset: este establece un contador con nombre en el valor actual de la propiedad, y counter() lo lee nuevamente. El fragmento es CSS aunque la fuente lo etiquete de otra manera.
.seconds-digit::after {
counter-reset: sc var(--s); /* set the "sc" counter = current --s value */
content: counter(sc, decimal-leading-zero); /* render as "00"–"59" */
}
.minutes-digit::after {
counter-reset: mc var(--m);
content: counter(mc, decimal-leading-zero);
}
.hours-digit::after {
counter-reset: hc var(--h);
content: counter(hc, decimal-leading-zero);
}
Cada vez que --s cambia a un nuevo número entero, el motor de estilos vuelve a calcular counter-reset, y content del pseudo-elemento se actualiza en consecuencia. No hay ningún script involucrado en esta actualización.
Paso 3: hacer que los valores cambien en pasos mediante steps()
Por defecto, una animación transita de forma continua entre los fotogramas clave. Un reloj no debe deslizarse; debe cambiar de dígito de forma abrupta. La función de temporización steps(N, end) divide la duración en N saltos discretos e iguales.
@keyframes tick-s {
from {
--s: 59;
}
to {
--s: -1;
}
}
animation: tick-s 60s steps(60, end) infinite;
Sexenta pasos distribuidos en una duración de sesenta segundos significan un salto por segundo: 59 al inicio, 58 después de un segundo, 0 después de 59 segundos, y vuelta a 59 cuando comienza la siguiente iteración.
La palabra clave end (equivalente a jump-end) coloca cada salto al final de su intervalo. Exactamente en t = 0, la animación sigue mostrando su valor from, ya que el primer salto aún no ha tenido lugar. Este detalle es importante cuando entran en juego retrasos negativos: un retraso que ocurre exactamente en t = 0 muestra 59 en lugar de 58.
Terminar los fotogramas clave en -1 parece extraño, pero es intencional. La animación interpola a lo largo de un rango de 60 valores enteros, y debido a jump-end, vuelve al valor inicial from antes de que aparezca el valor final. Por lo tanto, la secuencia visible consta exactamente de 59 valores que van desde 0 hacia abajo, en pasos uniformes de uno.
Cada unidad tiene sus propios fotogramas clave y cantidad de pasos. Los minutos requieren 60 pasos de 60 segundos cada uno, mientras que las horas necesitan 100 pasos de una hora cada uno:
@keyframes tick-s {
from {
--s: 59;
}
to {
--s: -1;
}
} /* 60 steps × 1s = 60s */
@keyframes tick-m {
from {
--m: 59;
}
to {
--m: -1;
}
} /* 60 steps × 60s = 1hr */
@keyframes tick-h {
from {
--h: 99;
}
to {
--h: -1;
}
} /* 100 steps × 1hr = 100hr */
Paso 4: comenzar a mitad de la secuencia con un retraso negativo
Este es el truco principal. Cuando animation-delay es negativo, el navegador considera que la animación comenzó hace ese número de segundos, por lo que comienza en el punto al que habría llegado hasta ahora.
Digamos que quedan 23 segundos en el minuto actual. La animación de los segundos comienza en 59 y ya debe haber disminuido 36 veces para mostrar 23; por lo tanto, un retraso de -36s la coloca en la posición correcta. En general:
delay = -(59 - s) where s = remaining seconds within the current minute
Para aplicar esto a las tres unidades, primero se necesita el tiempo total restante en segundos, calculado a partir de la marca de tiempo final:
const remaining = endTime - Math.floor(Date.now() / 1000);
Ese total se divide luego en las partes correspondientes a cada unidad. Las horas se limitan a 99 porque la animación de las horas solo abarca 100 pasos:
const s = remaining % 60;
const m = Math.floor(remaining / 60) % 60;
const h = Math.min(Math.floor(remaining / 3600), 99);
Tenga en cuenta esta limitación como una restricción de cálculo. Un cupón que vence en cinco días mostrará 99:59:59 y parecerá estancado hasta que queden menos de 100 horas. Si sus ofertas pueden durar tanto tiempo, agregue una unidad de días o cambie los formatos.
Mantener los dígitos sincronizados con un único retraso
El siguiente paso obvio es calcular un retraso separado por unidad. Esto genera un problema grave: los segundos vuelven a 59 mientras que los minutos permanecen sin cambiar durante demasiado tiempo.
Por qué los retrasos separados se desvían
Tomemos remaining = 125, que corresponde a 0 horas, 2 minutos y 5 segundos. Al calcular los retrasos de segundos y minutos de forma independiente se obtiene:
delayS = -(59 - 5) = -54s → seconds virtual start: now - 54s
delayM = -(59 - 2) × 60 = -3420s → minutes virtual start: now - 3420s
Cada animación mide sus saltos en relación con un tiempo de inicio virtual privado. Los pasos de segundos se activan cada segundo a partir de now - 54s; los pasos de minutos se activan cada 60 segundos a partir de now - 3420s. Dado que los puntos de partida son diferentes, los límites se producen en momentos distintos del reloj real. Aquí es donde el dígito de segundos alcanza su siguiente reinicio:
now - 54 + 60 = now + 6s
No obstante, el dígito de minutos no avanza hasta:
now - 3420 + (58 × 60) = now - 3420 + 3480 = now + 60s
Así, los segundos vuelven a comenzar después de seis segundos, mientras que los minutos cambian por completo cada minuto. Durante 54 segundos la pantalla muestra 01:59 cuando el valor real es inferior a 01:00. La hora está incorrecta durante casi todo cada minuto.
Un origen compartido
La solución es tener un origen compartido para las tres animaciones, obteniéndolo al combinar los desfases en un único retraso:
const delay = -((99 - h) * 3600 + (59 - m) * 60 + (59 - s));
// = -( 99×3600 + 57×60 + 54 )
// = -(356400 + 3420 + 54)
// = -359874s
Ahora cada animación se mide a partir de now - 359874s. Los límites entre sus pasos son múltiplos de su propia duración de paso, contados desde ese punto de origen. Observe el ejemplo: dentro de un período de 60 segundos, la animación de segundos se encuentra en la posición 54, por lo que su próximo límite llega después de 6 segundos. Dentro de un período de 3,600 segundos, la animación de minutos se encuentra en la posición 3,474, y su próximo límite está en 3,480, también a 6 segundos de distancia. Ambos valores cambian juntos en now + 6s y permanecen alineados cada 60 segundos a partir de entonces.
Cómo un número codifica tres dígitos
El retraso combinado funciona como un número de base mixta, de manera similar a cómo HH:MM:SS agrupa tres campos en una sola cadena. La codificación se realiza a partir de los tres componentes:
|delay| = (99 - h) × 3600 ← hours field, each unit = 3600s
+ (59 - m) × 60 ← minutes field, each unit = 60s
+ (59 - s) ← seconds field, each unit = 1s
La decodificación es lo que hace implícitamente cada animación al observar únicamente su propio período de tiempo:
seconds position = |delay| mod 60 → display 59 - position
minutes position = floor((|delay| mod 3600) / 60) → display 59 - position
hours position = floor(|delay| / 3600) → display 99 - position
Ejecutar el ejemplo con el decodificador lo confirma. 359,874 módulo 60 es 54, por lo que los segundos muestran 59 − 54 = 5. 359,874 módulo 3,600 es 3,474; al dividirlo entre 60 y tomar el entero resultante se obtiene 57, así que los minutos muestran 59 − 57 = 2. 359,874 dividido entre 3,600 y tomado el entero resultante es 99, por lo que las horas muestran 99 − 99 = 0. La pantalla muestra 00:02:05, lo cual es correcto.
Las animaciones no pueden interferir entre sí porque cada campo representa un múltiplo exacto del ciclo del campo siguiente: la parte de las horas es siempre un múltiplo de 3,600 y la parte de los minutos siempre es un múltiplo de 60, por lo que ninguna perturba a las demás.
Detenerse limpiamente en 00:00:00
Las animaciones de los ticks utilizan una iteración infinita, por lo que por sí solas nunca se detienen. Cuando termina el ciclo de los segundos, este vuelve a 59, y un temporizador que llegue a cero comenzaría de inmediato a contar hacia atrás nuevamente.
La solución consiste en una animación adicional que se reproduce una sola vez, exactamente en el momento de vencimiento, y establece las tres propiedades en cero. Este bloque también es CSS a pesar de su etiqueta original:
@keyframes freeze {
to {
--s: 0;
--m: 0;
--h: 0;
}
}
.timer {
animation-name: tick-s, tick-m, tick-h, freeze;
animation-duration: 60s, 3600s, 360000s, 1ms;
animation-timing-function: steps(60, end), steps(60, end), steps(100, end),
step-start;
animation-iteration-count: infinite, infinite, infinite, 1;
animation-fill-mode: none, none, none, forwards;
}
JavaScript asigna el retraso compartido a las tres animaciones de actualización y el tiempo restante, como un retraso positivo, a la animación de congelamiento:
el.style.animationDelay = `${delay}s, ${delay}s, ${delay}s, ${remaining}s`;
Aquí colaboran tres elementos clave:
step-start(lo mismo questeps(1, jump-start)) salta directamente al fotograma clave final tan pronto como comienza la animación, en lugar de esperar hasta el final de sus 1 ms de duración, de modo que los tres valores se convierten en 0 en el instante en que vence el retraso.animation-fill-mode: forwardsmantiene el fotograma clave final aplicado después de que termina la animación, por lo que los valores cero persisten indefinidamente.
animation-name es importante: cuando varias animaciones establecen la misma propiedad, gana la que se indica más tarde. Dado que freeze está al final, sus valores cero sobrescriben a las animaciones de actualización, que siguen repitiéndose en segundo plano sin efecto visible.Si el cupón ya ha vencido cuando se carga la página, el tiempo restante es 0, el retraso de congelación es 0s, y el temporizador muestra ceros de inmediato.
Qué te cuesta el enfoque CSS
Diseñar sin JavaScript tiene limitaciones reales, y estas deberían influir en tu decisión:
- Sin mecanismo de vencimiento. No existe ninguna función de callback, evento ni cambio de estado cuando el temporizador llega a cero. Si la expiración debe activar una llamada a API, ocultar la tarjeta o mostrar algo diferente, necesitas un
setTimeoutseparado, lo que resta algo de esa simplicidad. - Estilización restringida.
counter()genera texto plano. La fuente, el color y el diseño están completamente bajo su control, pero cualquier elemento que dependa del valor actual en JavaScript, como cambiar al rojo cuando quedan menos de 60 segundos o agregar una clase al alcanzar un umbral, requiere trabajo adicional. - Precisión de segundo completo. El tiempo restante se redondea a segundos completos al inicializarse, por lo que las marcas visibles pueden desviarse hasta un segundo de los límites reales de los segundos. Esto es relevante en cuentdowns promocionales, pero no se trata de un reloj de alta precisión.
- Soporte en navegadores. Esta técnica depende de
@propertyy de propiedades registradas animadas; confirme el soporte en su matriz de navegadores según los datos de compatibilidad actuales antes de utilizarla.
A cambio, al tratarse de un contador de regresión de solo lectura que solo necesita avanzar y detenerse, es extremadamente eficiente: un conjunto de animaciones por tarjeta, sin JavaScript en cada avance, sin desviaciones y nada que limpiar al desmontar. Destaca cuando una página renderiza una cuadrícula de cientos de tarjetas de cupón al mismo tiempo.
El componente React para el temporizador CSS
La versión en CSS cambia la forma del componente. No hay estado, ni bucle de animación ni tareas de limpieza. El componente lee el reloj una vez, escribe una propiedad de estilo y renderiza el marcado. Solo necesita dos hooks de React:
import { useEffect, useRef } from 'react';
El componente a continuación calcula los segundos restantes, los divide en unidades, construye el retraso compartido y escribe los cuatro retrasos en el elemento. La propiedad type solo cambia entre dos estilos visuales de los dígitos.
interface CSSCountdownTimerProps {
endTime: number; // Unix timestamp in seconds, same as existing prop
type?: 'solid' | 'regular';
}export function CSSCountdownTimer({
endTime,
type = 'solid',
}: CSSCountdownTimerProps) {
const timerRef = useRef<HTMLDivElement>(null); useEffect(() => {
const el = timerRef.current;
if (!el) return; const now = Math.floor(Date.now() / 1000);
const remaining = Math.max(0, endTime - now); const s = remaining % 60;
const m = Math.floor(remaining / 60) % 60;
const h = Math.min(Math.floor(remaining / 3600), 99); const delay = -((99 - h) * 3600 + (59 - m) * 60 + (59 - s)); el.style.animationDelay = `${delay}s, ${delay}s, ${delay}s, ${remaining}s`; // No cleanup needed.
// There are no JS timers, no rAF loops, no subscriptions.
// CSS animations stop automatically when the element is removed from the DOM.
}, [endTime]); return (
<div ref={timerRef} className={`timer ${type}`}>
<span
className={type === 'solid' ? 'digit hours-digit' : 'hours-digit'}
/>
<span className="sep">:</span>
<span
className={type === 'solid' ? 'digit minutes-digit' : 'minutes-digit'}
/>
<span className="sep">:</span>
<span
className={type === 'solid' ? 'digit seconds-digit' : 'seconds-digit'}
/>
</div>
);
}
Tal como indican los comentarios, no se requiere ninguna limpieza: cuando el elemento abandona el DOM, sus animaciones lo hacen junto con él. Hay dos mejoras que vale la pena considerar. Primero, como useEffect se ejecuta después de que el navegador dibuje la pantalla, el primer fotograma puede mostrar brevemente las animaciones en sus posiciones por defecto antes de que se aplique el retraso; useLayoutEffect, o calcular el retraso durante la renderización y pasarlo como un estilo en línea, evita ese destello. Segundo, el efecto depende de endTime, por lo que si la propiedad cambia, los retrasos se vuelven a calcular y las animaciones se reposicionan correctamente.
Elegir entre los tres enfoques
Una sencilla regla de decisión cubre la mayoría de las situaciones:
- Si algo debe ocurrir cuando expire el temporizador, como una actualización de estado, un nuevo recargo de datos o una llamada a la API, utilice
requestAnimationFramecon recálculo basado en el reloj. Esto le brinda control total y un lugar adecuado para la función de callback al vencimiento. - Si una página muestra muchos temporizadores y ninguno de ellos activa lógica alguna, recurra a la técnica CSS y deje que el navegador realice el trabajo.
- Si está utilizando una cadena simple de
setTimeoutque disminuye un contador, cámbielo. Es impreciso y sigue consumiendo recursos del hilo principal cada segundo, por lo que combina las desventajas de ambas alternativas. También se pueden combinar ambos enfoques: CSS para la visualización y un único temporizador compartido para la lógica de vencimiento.
Puntos clave
- Los retrasos de los temporizadores son valores mínimos; obtenga el tiempo restante a partir de
Date.now()en cada actualización en lugar de contar los ticks, y así desaparecerá la desviación.
@property, counter() para texto con relleno, steps() para saltos discretos y un retardo negativo para comenzar a mitad de secuencia.freeze para mantener los ceros.Lecturas relacionadas
- APIs nativas del navegador que sustituirán a paquetes populares de npm en 2026 — Explica cómo las características nativas de JavaScript y CSS como Signals, el operador pipeline, Temporal y el posicionamiento por anclaje están reemplazando a paquetes comunes de npm.