Create a weather app with JavaScript

Create a weather app with JavaScript

Learn how to build a weather app with JavaScript, Tailwind CSS and OpenWeatherMap API

As the title says, we are building a weather app. We are using OpenWeatherMap as a data provider and Tailwind CSS for designing UI elements. The logic is written in vanilla JS. You will learn how to read data from the API and how to start with Tailwind CSS.

Let's dive in. 👇

Generate API key

Go to OpenWeatherMap and create an account if you don't have one. After signing in, find API keys tab and generate a new key.

Tailwind

Now let's create an empty folder for our project. Now create a file tailwind.config.js and paste in it the following code:

/** @type {import('tailwindcss').Config} */ 
module.exports = {
  content: ["./src/**/*.{html,js}"],
  theme: {
    extend: {},
  },
  plugins: [],
}

Now create a folder named src. It will contain .html, .css and .js files. Open up your terminal and run:

npm install -D tailwindcss

npx tailwindcss init

Create these files inside the src folder:

  • index.html
  • input.css
  • app.js

Your project files structure should look like this:

file structure.png

Now inside your input.css file paste the following code:

@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css");

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

I am using Bootstrap icons, so I imported them at the top of the file. I also imported Tailwind directives that will be utilized.

In order to see the effects of Tailwind CSS, we have to start the build process by running this command in the terminal:

npx tailwindcss -i ./src/input.css -o ./dist/output.css --watch

This will create a dist folder and output.css file where the generated CSS will be stored.

We have to add this CSS file to the <head> of index.html.

<link href="/dist/output.css" rel="stylesheet">

HTML

Simply paste this code to your index.html:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Weather</title>
    <link href="/dist/output.css" rel="stylesheet">
</head>

<body>
    <div id="app" class="container-xl h-screen flex-col justify-center content-center">
        <h1 id="title" class="font-medium leading-tight text-center text-5xl mb-2">Weather app</h1>
        <br>
        <div id="search" class="block w-96 mx-auto bg-transparent">
            <form id="searchForm">   
                <div class="relative">
                    <div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
                        <svg aria-hidden="true" class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
                    </div>
                    <input type="search" name="city" id="searchInput" class="block p-4 pl-10 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Enter your city..." required>
                    <button type="submit" class="text-white absolute right-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Search</button>
                </div>
            </form>
        </div>
        <div id="weather" class="hidden">
            <div class="flex justify-center mt-2">
                <ul class="bg-white rounded-lg border border-gray-200 w-96 text-gray-900">
                    <li class="px-6 py-2 border-b border-gray-200 w-full rounded-t-lg flex justify-between">
                        <span>Location</span>
                        <span id="location"></span>
                    </li>
                    <li class="px-6 py-2 border-b border-gray-200 w-full flex justify-between">
                        <span>Description</span>
                        <span id="description"></span>
                    </li>
                    <li class="px-6 py-2 border-b border-gray-200 w-full flex justify-between">
                        <span>Temperature</span>
                        <span id="temperature"></span>
                    </li>
                    <li class="px-6 py-2 border-b border-gray-200 w-full flex justify-between">
                        <span>Temperature (min - max)</span>
                        <span id="temperatureMinMax"></span>
                    </li>
                    <li class="px-6 py-2 border-b border-gray-200 w-full flex justify-between">
                        <span>Feels like</span>
                        <span id="feelsLike"></span>
                    </li>
                    <li class="px-6 py-2 border-b border-gray-200 w-full flex justify-between">
                        <span>Clouds</span>
                        <span id="clouds"></span>
                    </li>
                    <li class="px-6 py-2 border-b border-gray-200 w-full flex justify-between">
                        <span>Wind</span>
                        <span id="wind"></span>
                    </li>
                </ul>
            </div>
        </div>
        <div id="repeatSearch" class="mt-3 w-96 mx-auto hidden">
            <div class="flex space-x-2 justify-center">
                <button type="button" class="block w-full px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded-lg shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out">
                    Repeat search
                    <span>
                        <i class="relative top-px text-base bi bi-arrow-clockwise"></i>
                    </span>
                </button>
            </div>
        </div>
        <div id="error" class="mt-3 w-96 mx-auto hidden">
            <h1 id="errorMessage" class="font-medium leading-tight text-center text-5xl my-4"></h1>
        </div>
    </div>

    <script src="app.js"></script>
</body>

</html>

The classes that are used here are Tailwind's utility classes. When you build your app, Tailwind generates the CSS code for the classes that are being used. This way you can quickly build your UI because Tailwind provides these easy-to-use primitive utilities, so you don't have to write CSS.

JavaScript

Now, let's write our js code.

const key = "*********************"; // <- your API key goes here

const app = document.querySelector("#app");
const title = document.querySelector("#title");
const searchContainer = document.querySelector("#search");
const searchInput = document.querySelector("#searchInput");
const searchForm = document.querySelector("#searchForm");
const weather = document.querySelector("#weather");
const repeatSearch = document.querySelector("#repeatSearch");
const errorMessage = document.querySelector("#errorMessage");
const errorContainer = document.querySelector("#error");

const RAINY = " bg-gradient-to-r from-cyan-500 via-blue-500 to-indigo-600";
const CLOUDY = " bg-gradient-to-l from-zinc-800 via-blue-900 to-purple-900";
const BASE = "container-xl h-screen flex-col justify-center content-center";

RAINY, CLOUDY and BASE constants are simply strings that contain multiple Tailwind classes that will be used later. This way it is easy to assign and remove these classes in the DOM.

We need an event listener to handle the search option.

searchForm.addEventListener("submit", (e) => {
  e.preventDefault();
  getWeather(searchInput.value);
});

function getWeather(location) {
  fetch(
    "https://api.openweathermap.org/data/2.5/weather?q=" +
      location +
      "&appid=" +
      key +
      "&units=metric"
  )
    .then(function (resp) {
      return resp.json();
    })
    .then(function (data) {
      display(data);
    })
    .catch(function (error) {
      console.log(error);
    });
}

We are passing the search input value to getWeather function so we can fetch the data for the desired location. We are converting the received data to json and passing it to display function.

function display(data) {
    hideSearch();
  if (data.cod === "404" || data.cod === 401) {
    showError(data.cod);
} else {
    document.querySelector("#description").innerHTML =
      data.weather[0].description;
    document.querySelector("#clouds").innerHTML = data.clouds.all + "%";
    document.querySelector("#temperature").innerHTML = data.main.temp + "&deg;";
    document.querySelector("#temperatureMinMax").innerHTML =
      data.main.temp_min + "&deg;" + " - " + data.main.temp_max + "&deg;";
    document.querySelector("#feelsLike").innerHTML =
      data.main.feels_like + "&deg;";
    document.querySelector("#location").innerHTML =
      data.name +
      ` <span class="inline-block py-1 px-2 leading-none text-center whitespace-nowrap align-baseline font-bold bg-blue-600 text-white rounded-lg">${data.sys.country}</span>`;
    document.querySelector("#wind").innerHTML = data.wind.speed + " m/s";

    if (data.weather[0].description.indexOf("rain") > 0) {
      app.className += RAINY;
      title.className += " text-white";
    } else if (data.weather[0].description.indexOf("cloud") > 0) {
      app.className += CLOUDY;
      title.className += " text-white";
    }
  }
}

function hideSearch() {
  searchContainer.classList.replace("block", "hidden");
  weather.classList.replace("hidden", "block");
  repeatSearch.classList.replace("hidden", "block");
  searchInput.value = "";
}

We are hiding the search box and displaying the information that was received from the API. I am showing it inside an unordered list because it is easy to design and it fits the purpose of the tutorial. If the data indicates that the weather is rainy or cloudy, we are adding the classes that we mentioned earlier to the background of the app so the atmosphere of the current weather is shown.

When it comes to showing an error or repeating a search, we are adding the following code:

function searchRepeat() {
  searchContainer.classList.replace("hidden", "block");
  weather.classList.replace("block", "hidden");
  repeatSearch.classList.replace("block", "hidden");
  errorContainer.classList.replace("block", "hidden");
  searchInput.value = "";
  title.classList.replace("text-white", "text-dark");
  if (
    app.className.includes(CLOUDY) ||
    app.className.includes(RAINY)
  ) {
    app.className = BASE;
  }
}

function showError(code) {
    if (code === "404") {
        errorMessage.innerHTML = "Oops... city not found.";
    } else if (code === 401) {
        errorMessage.innerHTML = "Please enter you OpenWeatherMap API key.";
    }

    errorContainer.classList.replace("hidden", "block");
    searchContainer.classList.replace("block", "hidden");
    weather.classList.replace("block", "hidden");
    repeatSearch.classList.replace("hidden", "block");
}

repeatSearch.addEventListener("click", () => {
  searchRepeat();
});

So, that's it. 👍

Thank you for reading and you can find the entire app on GitHub.