Flip Countdown

Flip-clock countdown to any date, with days, hours, minutes and seconds.

--

Days

--

Hours

--

Minutes

--

Seconds

Edit with AIPro

Describe a change to Flip Countdown and get the updated code. The live preview above stays as the original.

Installation

  1. Install the dependencies

    $ npm i motion lucide-react
  2. Copy the source into components/ui/flip-countdown.tsx

    components/ui/flip-countdown.tsx
    "use client";
    
    import { AnimatePresence, motion } from "motion/react";
    import { useEffect, useState } from "react";
    
    function Card({ value }: { value: string }) {
      return (
        <span className="relative inline-block h-16 w-12 overflow-hidden rounded-xl bg-zinc-900 text-4xl font-semibold tabular-nums text-white shadow-lg [perspective:300px] sm:h-20 sm:w-14 sm:text-5xl">
          <AnimatePresence mode="popLayout" initial={false}>
            <motion.span
              key={value}
              initial={{ rotateX: -90, opacity: 0 }}
              animate={{ rotateX: 0, opacity: 1 }}
              exit={{ rotateX: 90, opacity: 0 }}
              transition={{ duration: 0.35, ease: "easeOut" }}
              className="absolute inset-0 grid origin-center place-items-center"
            >
              {value}
            </motion.span>
          </AnimatePresence>
          <span aria-hidden className="absolute inset-x-0 top-1/2 h-px bg-black/50" />
        </span>
      );
    }
    
    /** Flip-clock countdown to a date. Renders dashes until mounted so static HTML and the browser agree. */
    export function FlipCountdown({ to }: { to: Date | number }) {
      const target = typeof to === "number" ? to : to.getTime();
      const [left, setLeft] = useState<number | null>(null);
    
      useEffect(() => {
        const tick = () => setLeft(Math.max(0, target - Date.now()));
        tick();
        const t = setInterval(tick, 1000);
        return () => clearInterval(t);
      }, [target]);
    
      const s = left === null ? null : Math.floor(left / 1000);
      const parts: [string, number | null][] = [
        ["Days", s === null ? null : Math.floor(s / 86400)],
        ["Hours", s === null ? null : Math.floor(s / 3600) % 24],
        ["Minutes", s === null ? null : Math.floor(s / 60) % 60],
        ["Seconds", s === null ? null : s % 60],
      ];
    
      return (
        <div className="flex items-start gap-3 sm:gap-5" role="timer" aria-live="off">
          {parts.map(([label, v]) => {
            const text = v === null ? "--" : String(v).padStart(2, "0");
            return (
              <div key={label} className="text-center">
                <div className="flex gap-1">
                  <Card value={text[0]} />
                  <Card value={text[1]} />
                </div>
                <p className="mt-2 text-[11px] font-medium uppercase tracking-widest text-zinc-500">{label}</p>
              </div>
            );
          })}
        </div>
      );
    }
    
    export default function Demo() {
      // 3 days, 4 hours and a bit from when the page opens.
      const [to] = useState(() => Date.now() + (3 * 86400 + 4 * 3600 + 125) * 1000);
      return <FlipCountdown to={to} />;
    }
    
  3. Use it

    The file exports the component and a Demo showing typical usage. Import whichever you need.