Let's build Launch Countdown Timer using Next.js & Tailwind CSS.

In this tutorial, I will show you how you can build a Launch Countdown Timer. It is one of the Frontend Mentor challenge projects.

Featured Image for Blog Post
Featured Image for Blog Post

Hello everyone,  I hope you all are doing well. Today again I am back with another exciting web dev project, which will help to learn some new web dev skills as a Front-end developer.


In this tutorial, I will show you how you can build a Launch Countdown Timer. It is one of the  Frontend Mentor challenge projects and our goal is to make it look like the design given by the Frontend Mentor.

Here is the link to the FrontendMentor challenge that we will build.

So without any further talk, Let's start building it 🚀.

Step-1 Initializing the Project

Create a new next-js app with Tailwind CSS bootstrapped in it.

You can use this one-line command to create a new next.js app with TypeScript and Tailwind CSS.

npx create-next-app -e with-tailwindcss my-project-name

You can name your project whatever you want, I will name it as Launch countdown Timer.

💡
We are using TypeScript in this Project, so we have to explicitly define all the props, constants & function data types.

Now after creating the project open it in Vs Code or any IDE that you prefer.

Find the index.tsx file inside pages directory. and delete everything and paste the given code below.

import type { NextPage } from 'next'
import Head from 'next/head'

const Home: NextPage = () => {

return (
    <div className="flex min-h-screen flex-col items-center bg-[#1e1f29]">
      <Head>
        <title>Launch Countdown Timer</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>


)}

export default Home;

After that visit the globals.css file inside the styles folder and import a google font link (Red Hat Font) at the top of the file inside it.

@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800;900&family=Red+Hat+Text:wght@500;700&display=swap');


@tailwind base;
@tailwind components;
@tailwind utilities;

After that go to the tailwindcss.config.js file and we will add some custom font and animation that we will use later in the project.


module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {
      animation: {
        flip:'flip 1s cubic-bezier(0, 0, 0.2, 1) infinite'
      },
      keyframes:{
        flip: {
          'from': { transform:'rotateX(0deg)', transformOrigin: '50% bottom ',},
          'to':{transform:  'rotateX(180deg)', transformOrigin: '50% bottom ',}
        }
      }
    },
       fontFamily: {
      'redhat':['Red Hat Text', 'sans-serif'],
      },
  plugins: [],
}

}

So, that was all for the first step, Now moving to the second step.

Step-2 Creating the Components

Now, it is time to create some components that we are going to use in the app.

Create a new folder name components in the root of the directory  and create 5 files inside it.

  1. Header.tsx
  2. Footer.tsx
  3. NumberBox.tsx
  4. TimerContainer.tsx
  5. TimerInput.tsx

After creating the file we will start adding the code to each file one by one.

Inside Header.tsx

import React from 'react'

interface msgProps {
  message: string,
};

export const Header = ({ message }: msgProps) => {
  return (
    <header className="mx-auto mt-2">
      <h1 className="text-2xl mt-8 md:text-4xl font-bold font-redhat text-rose-500 font-bold text-center mx-auto ">
        {message ? message : "WE'RE LAUNCHING SOON"}
      </h1>
    </header>
  )

Inside Footer.tsx

import React from 'react'

export const Footer = () => {
  return (
    <div className="mx-auto w-full ">
      <div className=" w-full flex mt-10 md:mt-40 space-x-6 justify-center items-center ">
        <a href="https://facebook.com/raivikas200" target="_blank">
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" className="h-7 w-7 md:h-10 md:w-10 hover:cursor-pointer" fill="rgb(244 63 94)">
            <path d="M504 256C504 119 393 8 256 8S8 119 8 256c0 123.78 90.69 226.38 209.25 245V327.69h-63V256h63v-54.64c0-62.15 37-96.48 93.67-96.48 27.14 0 55.52 4.84 55.52 4.84v61h-31.28c-30.8 0-40.41 19.12-40.41 38.73V256h68.78l-11 71.69h-57.78V501C413.31 482.38 504 379.78 504 256z" />
          </svg>
        </a>
        <a href="https://github.com/raiv200" target="_blank">
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" className="h-7 w-7 md:h-10 md:w-10 hover:cursor-pointer" fill="rgb(244 63 94)">
            <path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
          </svg>
        </a>

        <a href="https://twitter.com/raivikas200" target="_blank">
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" className="h-7 w-7 md:h-10 md:w-10 hover:cursor-pointer" fill="rgb(244 63 94) ">
            <path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z" />
          </svg>
        </a>
      </div>
    </div>
  )
}


Inside NumberBox.tsx

import React from 'react'

interface numProp {
    num: string | number,
    unit: string,
    flip: boolean,
};

export const NumberBox = ({ num, unit, flip }: numProp) => {
    return (
        <div className="flex flex-col items-center mt-4 px-2">
            <div className=" relative bg-transparent flex flex-col items-center justify-center rounded-lg w-32 h-32  text-2xl md:text-4xl mt-4 ">
                <div className="rounded-t-lg rounded-b-lg bg-[#343650] w-full h-full"></div>

                <div className="text-5xl absolute text-rose-500 z-10 font-bold font-redhat md:text-7xl font-mono ">
                    {num}
                </div>

                <div className=" rounded-b-lg rounded-t-lg bg-[#2c2e3f] w-full h-full"></div>

                <div className={`absolute  w-full h-1/2 top-0  rounded-t-lg z-5 ${flip ? 'animate-flip bg-rose-200' : 'bg-transparent'}`}></div>
                {/* Two Small Dots */}
                <div className="absolute -right-1 top-[60px] rounded-full w-[12px] h-[12px] bg-[#1e1f29]"></div>
                <div className="absolute -left-1 top-[60px] rounded-full w-[12px] h-[12px] bg-[#1e1f29]" ></div>

            </div>
            <p className="text-lg mt-3 font-semibold text-rose-200  md:text-2xl ">
                {unit}
            </p>
        </div>
    )
}

Inside TimerContainer.tsx

import React from 'react'
import { NumberBox } from './NumberBox'

interface timeProps{
  days: number | string,
  hours:number | string ,
  minutes:number | string,
  seconds:number | string,
}

export const TimerContainer = ({days, hours, minutes ,seconds }: timeProps) => {

  let daysFlip = false;
  let hoursFlip = false;
  let minutesFlip = false;
  let secondsFlip = true;

 if (seconds <=0 && minutes <=0 && hours <=0 && days <=0){
   daysFlip =  false;
   hoursFlip =  false;
   minutesFlip = false;
   secondsFlip = false;
 }

 if(seconds == 0){
   if( minutes !=0){
    seconds=59;
   }
   
   secondsFlip = false;
   minutesFlip = true;
 }
 if (minutes == 0 ){
    if( hours !=0){
      minutes=59;
    }
   
   minutesFlip = false;
   hoursFlip = true;
 }

 if( hours == 0){
   hoursFlip = false;
   if(days !=0){
     daysFlip = true;
   }
   
 }

 

   if(days <10){
     days="0"+days
   }

   if(hours <10){
     hours="0"+hours
   }

   if(minutes <10){
     minutes="0"+minutes
   }

   if(seconds < 10){
     seconds="0"+seconds
     
   }
  
    return (

     <div className=" mt-2 md:mt-20  rounded-xl">
       <div className="grid grid-cols-2 gap-4 py-6 px-10 md:flex md:items-center md:justify-between md:mt-2  rounded-xl md:px-6 md:py-8 ">
            <NumberBox num={days } unit="Days" flip={daysFlip} />
            <span className=" hidden text-5xl -mt-8 md:inline-block md:text-7xl font-normal text-gray-50 ">:</span>
            <NumberBox num={hours } unit="Hours" flip={hoursFlip} />
            <span className="hidden text-5xl -mt-8 md:inline-block md:text-7xl font-normal text-gray-50 ">:</span>
            <NumberBox num={minutes} unit="Minutes" flip={minutesFlip}/>
            <span className="hidden text-5xl -mt-8 md:inline-block md:text-7xl font-normal text-gray-50 ">:</span>
            <NumberBox num={seconds} unit="Seconds" flip={secondsFlip} />
        </div>
   
      </div>
    )
}

Inside TimerInput.tsx

import React from 'react'


interface inputProps {
    value:number,
    handleClick() : void,
    handleChange(e:any) : void,
}

export const TimerInput = ({value , handleClick ,handleChange}: inputProps) => {
  return (
    <div className="z-6 mx-auto space-y-4 flex flex-col md:flex-row justify-center items-center md:space-y-0">
        <input 
        className="text-xl md:text-2xl font-redhat outline-none px-2 py-1 w-40 rounded-lg mr-4 "
        name="Timer Input"
        type="number" 
        placeholder="Enter No. of Days" 
        value={value}
        onChange={handleChange}
        min={0}
        />

      <button onClick={handleClick} className="bg-rose-300 text-xl font-semibold font-redhat px-4 py-2 md:text-xl rounded-xl text-rose-500 hover:bg-rose-500 hover:text-rose-100 transition duration-300 ease-in"> Set Value</button>
    </div>
  )
}

After creating the components and adding the above code, Import all the components inside the index.tsx file.

You will notice that you are getting some errors in the files that the props you are using are not defined.

It is because of TypeScript since it is a strongly typed language, each and everything has to be defined.

But don't worry as soon as we complete the code the error will go.

The index.tsx file will look like this now:

import type { NextPage } from 'next'
import Head from 'next/head'
import { TimerContainer } from '../components/TimerContainer'
import { Footer } from '../components/Footer'
import { Header } from '../components/Header'
import { TimerInput } from '../components/TimerInput'

const Home: NextPage = () => {

return (
    
<div className="flex min-h-screen flex-col items-center bg-[#1e1f29]">
      <Head>
        <title>Launch Countdown Timer</title>
        <link rel="icon" href="/favicon.ico" />

      </Head>

      <Header message={message} />

      <TimerContainer
        days={days}
        hours={hours}
        minutes={minutes}
        seconds={seconds}
      />
      <TimerInput value={newTime} handleClick={handleClick} handleChange={handleChange} />

      <Footer />
    </div>

)}

export default Home;

As you can see, I am passing various props to the components.

Now, in the next step, we are going to create some variables using the useState Hook and write the logic of the timer.

Step-3 Code Logic for the Countdown Timer

Open the index.tsx file and inside the Home component create some variable.

And after that, we will add the code logic for the countdown timer inside Home Component above the return statement.

Inside Home Component (index.tsx):

  const [newTime, setNewTime] = useState<number>(0)
  const [days, setDays] = useState<number>(0);
  const [hours, setHours] = useState<number>(0);
  const [minutes, setMinutes] = useState<number>(0);
  const [seconds, setSeconds] = useState<number>(0);
  const [message, setMessage] = useState<string>("");

 const timeToDays = time * 60 * 60 * 24 * 1000;

 let countDownDate = new Date().getTime() + timeToDays;

💡
Make sure to import the useState hook and also useEffect Hook from react.

Now below the above code, we will use the setInterval function inside the useEffect hook, which will update the time every second.

useEffect(() => {


    var updateTime = setInterval(() => {
      var now = new Date().getTime();

      var difference = countDownDate - now;

      var newDays = Math.floor(difference / (1000 * 60 * 60 * 24));
      var newHours = Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
      var newMinutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60));
      var newSeconds = Math.floor((difference % (1000 * 60)) / 1000);

      setDays(newDays);
      setHours(newHours);
      setMinutes(newMinutes);
      setSeconds(newSeconds);


      if (difference <= 0) {
        clearInterval(updateTime);
        setMessage("The Launch Has Started");
        setDays(0);
        setHours(0);
        setMinutes(0);
        setSeconds(0);
      }
    })

    return () => {
      clearInterval(updateTime);
    }

  }, [time]);

As you can see we have also used the clearInterval() function which will clear or stop the setInerval() function as soon as the countdown time reaches zero.

The step-3 is done but two more things are remaining.

We have to create two functions handleClick() and handleChange() function which will be used to set the countdown timer as per the user-entered input.

Step-4 Creating the two functions handleClick() & handlechange().


After the end of the useEffect function, create two functions below it handleChange() and handleClick() like this:

const handleClick = () => {

    setTime(newTime);
    console.log(time);
    setNewTime(0);
  };

  const handleChange = (e: any) => {
    let inputTime = e.target.value;
    setNewTime(inputTime);

  };

Now for the final step, we will add the flip animation.

Step-5 Adding the Flip Animation


The flip animation will be added conditionally, I have already added the code for the animation in advance inside the TimerContainer.tsx and the NumberBox.tsx components in Step-2.

You can take a look at the code and try to understand it on your own as a challenge.

It's not that difficult to understand the conditional logic that I have used to conditionally show the Flips.

Congratulations 🎉 🥳 !!!

Everything is done and you have successfully created awesome projects.

Now open the terminal inside VS code and start the development server by running the npm run dev command.

And visit localhost:3000 and your application is ready.

Conclusion


Hope you were able to build this Launch Countdown Timer.  Feel free to follow me on Twitter and share this if you like this project 😉.

I hope you like this project and enjoyed building it, I would appreciate ✌️ it if you could share this blog post.

If you think that this was helpful and then please do consider visiting my blog website nextjsdev.com and do follow me on Twitter and connect with me on LinkedIn.

If you were stuck somewhere and not able to find the solution you can check out my completed Github Repo here.

Thanks for your time to read this project, if you like this please share it on Twitter and Facebook or any other social media and tag me there.

I will see you in my next blog ✌️. Till then take care and keep building projects.

Some Useful Link:

Connect with me: