Docs
Shifting Countdown
A Shifting Countdown Component dynamically updates and transitions between countdown values with smooth animations for an engaging user experience.
Installation
Install dependencies
npm install framer-motion lucide-react
Run the following command
It will create a new file shifting-countdown.tsx
inside the components/mage-ui/widget
directory.
mkdir -p components/mage-ui/widget && touch components/mage-ui/widget/shifting-countdown.tsx
Paste the code
Open the newly created file and paste the following code:
import { useAnimate } from "framer-motion";
import { useEffect, useRef, useState } from "react";
// NOTE: Change this date to whatever date you want to countdown to :)
const COUNTDOWN_FROM = "2024-10-01";
const SECOND = 1000;
const MINUTE = SECOND * 60;
const HOUR = MINUTE * 60;
const DAY = HOUR * 24;
type CountdownUnit = "Day" | "Hour" | "Minute" | "Second";
interface CountdownItemProps {
unit: CountdownUnit;
text: string;
}
const ShiftingCountdown = (): JSX.Element => {
return (
<div className="bg-gradient-to-br from-violet-600 to-indigo-600 p-4">
<div className="mx-auto flex w-full max-w-6xl items-center bg-white">
<CountdownItem unit="Day" text="days" />
<CountdownItem unit="Hour" text="hours" />
<CountdownItem unit="Minute" text="minutes" />
<CountdownItem unit="Second" text="seconds" />
</div>
</div>
);
};
const CountdownItem = ({ unit, text }: CountdownItemProps): JSX.Element => {
const { ref, time } = useTimer(unit);
return (
<div className="flex h-24 w-[100vw] flex-col items-center justify-center gap-1 border-r-[1px] border-slate-200 font-mono md:h-36 md:gap-2">
<div className="relative w-full overflow-hidden text-center">
<span
ref={ref}
className="block text-2xl font-medium text-black md:text-4xl lg:text-6xl xl:text-7xl"
>
{time}
</span>
</div>
<span className="text-xs font-light text-slate-500 md:text-sm lg:text-base">
{text}
</span>
</div>
);
};
interface UseTimerResult {
ref: React.RefObject<HTMLElement>;
time: number;
}
// NOTE: Framer motion exit animations can be a bit buggy when repeating
// keys and tabbing between windows. Instead of using them, we've opted here
// to build our own custom hook for handling the entrance and exit animations
const useTimer = (unit: CountdownUnit): UseTimerResult => {
const [ref, animate] = useAnimate();
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const timeRef = useRef<number>(0);
const [time, setTime] = useState<number>(0);
useEffect(() => {
intervalRef.current = setInterval(handleCountdown, 1000);
return () => clearInterval(intervalRef.current || undefined);
}, []);
const handleCountdown = async (): Promise<void> => {
const end = new Date(COUNTDOWN_FROM);
const now = new Date();
const distance = +end - +now;
let newTime = 0;
if (unit === "Day") {
newTime = Math.floor(distance / DAY);
} else if (unit === "Hour") {
newTime = Math.floor((distance % DAY) / HOUR);
} else if (unit === "Minute") {
newTime = Math.floor((distance % HOUR) / MINUTE);
} else {
newTime = Math.floor((distance % MINUTE) / SECOND);
}
if (newTime !== timeRef.current) {
// Exit animation
await animate(
ref.current,
{ y: ["0%", "-50%"], opacity: [1, 0] },
{ duration: 0.35 }
);
timeRef.current = newTime;
setTime(newTime);
// Enter animation
await animate(
ref.current,
{ y: ["50%", "0%"], opacity: [0, 1] },
{ duration: 0.35 }
);
}
};
return { ref, time };
};
export default ShiftingCountdown;