Build a Simple Weather App With Vanilla JavaScript

We have a lot of interesting things to cover, so grab a cup of coffee and let’s get started! Here’s a quick navigation menu for you.

What You’ll Learn In This Weather API JavaScript Tutorial:

  1. Scaffolding the Weather App

    1. Find a Weather API
    2. What is OpenWeather?
    3. Where To Find Weather Icons and Weather UI Kits

  2. Define the Page Markup
  3. Specify Some Basic Styles
  4. Set the Main Styles
  5. Add the JavaScript
  6. Bonus: Get Real-Time Weather Data With the weatherstack API

How to Build a Weather App

Ready to start building a weather app with JavaScript? We’ll walk you through crafting the interface, harnessing the power of APIs to fetch live data, and finally, displaying it all in a clear and informative way.

Here’s an introductory video which demonstrates the functionality of the app that we’re going to create:


Here’s the demo on CodePen for you to fork and play with:

This tutorial assumes that you’re familiar with AJAX, an essential front-end technique. If you’re just beginning, check out this series.

1. Scaffolding the Weather App

Before start creating our app, there are a few things that we have to take into consideration.

Find a Weather API

First things first, we have to find a provider that will let us incorporate its weather data into our app. Luckily enough, there are several different providers out there for developing weather apps. Most of them include a free package along with premium subscriptions that scale depending on the services/features. 

What is OpenWeatherMap?

In our case, we’re going to use OpenWeatherMap, one of the most popular free choices. OpenWeatherMap describes itself as a group of IT experts and data scientists that does deep weather data science since 2014. For each point on Earth, OpenWeather provides reliable historical, current and forecasted weather data via light-speed APIs.

To take advantage of its capabilities, first, we have to sign up for an OpenWeatherMap API key:

Register for an APIRegister for an APIRegister for an API

This service comes with different packages. As you can see from the visualization below, the starter (free) one allows 60 calls per minute which suits our needs:

The available packages for the OpenWeatherMap APIThe available packages for the OpenWeatherMap APIThe available packages for the OpenWeatherMap API

So before continuing, please make sure that you’ve registered for an OpenWeatherMap API key. Later, we’ll include that key in our script. 

Keep in mind that the best way to test the app is by forking the CodePen demo and including your own key. If we all share the same key, the app will probably not work due to API call limits.

Where To Find Weather Icons and Weather UI Kits 

Before we start with the weather API JavaScript code, we’ll need weather app icons. It’s worth noting that OpenWeatherMap comes with its own icon set and we’ll take a look at those. However, we’ll go one step further and use some custom ones. 

Now, if you’re a web designer or work on multiple web design projects, Envato Elements is the best option for you.

Envato Elements Weather App IconsEnvato Elements Weather App IconsEnvato Elements Weather App Icons

For a low monthly fee, this subscription-based marketplace gives you unlimited access to everything you’ll need for your projects, including top weather icon vector sets and weather UI kits.

Keep browsing Envato Elements weather UI kit library for more!

2. Define the Page Markup

Now that we’ve seen some top weather app icons, it’s time to build a weather website with JavaScript. We’ll define two sections. 

The first section will include a heading, a search form, and an empty span element. This element will become visible with an appropriate message under certain conditions. Specifically, if there isn’t any weather data available for a requested city or the data for this city are already known. 

The second section will include a list of cities. By default, it won’t contain any cities. But, as we start searching for the weather for a specific city, if weather data is available, a corresponding list item (city) will be appended to the unordered list.

Here’s the initial page markup:

1
<section class="top-banner">
2
  <div class="container">
3
    <h1 class="heading">Simple Weather App</h1>
4
    <form>
5
      <input type="text" placeholder="Search for a city" autofocus>
6
      <button type="submit">SUBMIT</button>
7
      <span class="msg"></span>
8
    </form>
9
  </div>
10
</section>
11
<section class="ajax-section">
12
  <div class="container">
13
    <ul class="cities"></ul>
14
  </div>
15
</section>

Note: In our CodePen demo, the autofocus attribute of the search field won’t work. In fact, it’ll throw the following error which you can see if you open your browser console:

The cross-origin error due to the autofocus attributeThe cross-origin error due to the autofocus attributeThe cross-origin error due to the autofocus attribute

However, if you run this app locally (not as a CodePen project), this issue won’t exist.

And here’s the markup associated with a list item that we’ll generate dynamically through JavaScript:

1
<li class="city">
2
  <h2 class="city-name" data-name="...">
3
    <span>...</span>
4
    <sup>...</sup>
5
  </h2>
6
  <span class="city-temp">...<sup>°C</sup></span>
7
  <figure>
8
    <img class="city-icon" src="..." alt="...">
9
    <figcaption>...</figcaption>
10
  </figure>
11
</li>

3. Specify Some Basic Styles

With the markup for the weather app ready, we’ll forge on with the CSS. The first step, as always, is to specify some CSS variables and common reset styles:

1
:root {
2
  --bg_main: #0a1f44;
3
  --text_light: #fff;
4
  --text_med: #53627c;
5
  --text_dark: #1e2432;
6
  --red: #ff1e42;
7
  --darkred: #c3112d;
8
  --orange: #ff8c00;
9
}
10

11
* {
12
  margin: 0;
13
  padding: 0;
14
  box-sizing: border-box;
15
  font-weight: normal;
16
}
17

18
button {
19
  cursor: pointer;
20
}
21
 
22
input {
23
  -webkit-appearance: none;
24
}
25
 
26
button,
27
input {
28
  border: none;
29
  background: none;
30
  outline: none;
31
  color: inherit;
32
}
33

34
img {
35
  display: block;
36
  max-width: 100%;
37
  height: auto;
38
}
39

40
ul {
41
  list-style: none;
42
}
43

44
body {
45
  font: 1rem/1.3 "Roboto", sans-serif;
46
  background: var(--bg_main);
47
  color: var(--text_dark);
48
  padding: 50px;
49
}

4. Set the Main Styles

Let’s now discuss the main styles of our weather app.

Section #1 Styles

First, we’ll add some straightforward styles to the elements of the first section. 

On medium screens and above (>700px) the layout should look like this:

The layout of the first section on large screensThe layout of the first section on large screensThe layout of the first section on large screens

On smaller screens the form elements will split into two lines:

The layout of the first section on small screensThe layout of the first section on small screensThe layout of the first section on small screens

Here are the associated styles:

1
/*CUSTOM VARIABLES HERE*/
2

3
.top-banner {
4
  color: var(--text_light);
5
}
6

7
.heading {
8
  font-weight: bold;
9
  font-size: 4rem;
10
  letter-spacing: 0.02em;
11
  padding: 0 0 30px 0;
12
}
13

14
.top-banner form {
15
  position: relative;
16
  display: flex;
17
  align-items: center;
18
}
19

20
.top-banner form input {
21
  font-size: 2rem;
22
  height: 40px;
23
  padding: 5px 5px 10px;
24
  border-bottom: 1px solid;
25
}
26

27
.top-banner form input::placeholder {
28
  color: currentColor; 
29
}
30

31
.top-banner form button {
32
  font-size: 1rem;
33
  font-weight: bold;
34
  letter-spacing: 0.1em;
35
  padding: 15px 20px;
36
  margin-left: 15px;
37
  border-radius: 5px;
38
  background: var(--red);
39
  transition: background 0.3s ease-in-out;
40
}
41

42
.top-banner form button:hover {
43
  background: var(--darkred);
44
}
45

46
.top-banner form .msg {
47
  position: absolute;
48
  bottom: -40px;
49
  left: 0;
50
  max-width: 450px;
51
  min-height: 40px;
52
}
53

54
@media screen and (max-width: 700px) {
55
  .top-banner form {
56
    flex-direction: column;
57
  }
58
  
59
  .top-banner form input,
60
  .top-banner form button {
61
    width: 100%;
62
  }
63

64
  .top-banner form button {
65
    margin: 20px 0 0 0;
66
  }
67
  
68
  .top-banner form .msg {
69
    position: static;
70
    max-width: none;
71
    min-height: 0;
72
    margin-top: 10px;
73
  }
74
}

Section #2 Styles

We’ll use CSS Grid to lay out the list items. Remember that each list item will represent a city. Their width will depend on the screen size.

On large screens (>1000px) we’ll have a four column layout.

The unordered list layout on large screensThe unordered list layout on large screensThe unordered list layout on large screens

Then on medium screens (>700px and ≤1000px) a three column layout, on small screens (>500px and ≤700px) a two column layout, and finally on extra small screens (≤500px) all elements will be stacked.

Here are the corresponding styles:

1
.ajax-section {
2
  margin: 50px 0 20px;
3
}
4

5
.ajax-section .cities {
6
  display: grid;
7
  grid-gap: 32px 20px;
8
  grid-template-columns: repeat(4, 1fr);
9
}
10

11
@media screen and (max-width: 1000px) { 
12
  .ajax-section .cities {
13
    grid-template-columns: repeat(3, 1fr);
14
  }
15
}
16

17
@media screen and (max-width: 700px) {
18
  .ajax-section .cities {
19
    grid-template-columns: repeat(2, 1fr);
20
  }
21
}
22

23
@media screen and (max-width: 500px) {  
24
  .ajax-section .cities {
25
    grid-template-columns: repeat(1, 1fr);
26
  }
27
}

Each column will look like a card with a bottom shadow that will be added via the ::after pseudo-element. 

Inside the card, we’ll place weather information about the requested city. These will come from our request, apart from the icons. Those icons, which as mentioned above, are grabbed from Envato Elements, will show the current weather condition of this city and match the equivalent OpenWeatherMap icons.

The card layout

Below you can see a part of the CSS needed for this layout:

1
/*CUSTOM VARIABLES HERE*/
2

3
.ajax-section .city {
4
  position: relative;
5
  padding: 40px 10%;
6
  border-radius: 20px;
7
  background: var(--text_light);
8
  color: var(--text_med);
9
}
10

11
.ajax-section .city::after {
12
  content: ’’;
13
  width: 90%;
14
  height: 50px;
15
  position: absolute;
16
  bottom: -12px;
17
  left: 5%;
18
  z-index: -1;
19
  opacity: 0.3;
20
  border-radius: 20px;
21
  background: var(--text_light);
22
}
23

24
.ajax-section figcaption {
25
  margin-top: 10px;
26
  text-transform: uppercase;
27
  letter-spacing: 0.05em;
28
}
29

30
.ajax-section .city-temp {
31
  font-size: 5rem;
32
  font-weight: bold;
33
  margin-top: 10px;
34
  color: var(--text_dark);
35
}
36

37
.ajax-section .city sup {
38
  font-size: 0.5em;
39
}
40

41
.ajax-section .city-name sup {
42
  padding: 0.2em 0.6em;
43
  border-radius: 30px;
44
  color: var(--text_light);
45
  background: var(--orange);
46
}
47

48
.ajax-section .city-icon {
49
  margin-top: 10px;
50
  width: 100px;
51
  height: 100px;
52
}

5. Add the JavaScript

At this point, we’re ready to build the core functionality of our weather app. Let’s do it!

On Form Submission

Each time a user submits the form by pressing the Enter key or the Submit button, we’ll do two things:

  1. Stop the form from submitting, hence prevent reloading the page.
  2. Grab the value which is contained in the search field.

Here’s the starting code:

1
const form = document.querySelector(".top-banner form");
2

3
form.addEventListener("submit", e => {
4
  e.preventDefault();
5
  const inputVal = input.value;
6
});

Next, we’ll check to see whether there are list items (cities) inside the second section. 

Perform an AJAX Request

We’ll start with the assumption that the list is empty. That said, it has never run any AJAX request in the past. In such a case, we’ll execute a request to the OpenWeatherMap API and pass the following parameters:

  1. The city name (e.g. athens) or the comma-separated city name along with the country code (e.g. athens,gr) which will be the value of the search field
  2. The API key. Again, you should use your own key to avoid unexpected errors due to API call limits.
  3. The unit of temperature for the requested city. In our case, we’ll go with Celsius.

With all the above in mind, by following the API documentation, our request URL should look something like this:

1
const apiKey = "YOUR_OWN_KEY";
2
const inputVal = input.value;
3

4
...
5

6
const url = `https://api.openweathermap.org/data/2.5/weather?q=${inputVal}&appid=${apiKey}&units=metric`;

To perform the AJAX request, we have a lot of options. We can use the plain old XMLHttpRequest API, the newer Fetch API, or even a JavaScript library like jQuery and Axios. For this example, we’ll go with the Fetch API. 

To grab the desired data, we have to do the following things:

  • Pass the URL we want to access to the fetch() method. 
  • This method will return a Promise containing the response (a Response object). But this won’t be the actual response, just an HTTP response. To grab the response data in the desired JSON format (this is the default data format of OpenWeatherMap), we’ll use Response object’s json() method.
  • This method will return another Promise. When it’s fulfilled, the data will be available for manipulation.
  • If for some reason the request is unsuccessful, a corresponding message will appear on the screen.

So, our AJAX request would look something like this:

1
...
2

3
 fetch(url)
4
  .then(response => response.json())
5
  .then(data => {
6
    // do stuff with the data
7
  })
8
  .catch(() => {
9
    msg.textContent = "Please search for a valid city 😩";
10
  });

Tip: Instead of chaining then()s, we could have used the newer and more readable async/await approach for the AJAX request.

Here’s an example of the response data:

An example of the response dataAn example of the response dataAn example of the response data

Build the List Item Component

With the AJAX request in place, each time we type a city in the search field, the API will return its weather data, if they are available. Our job now is to collect only the data that we need, then create the associated list item and, lastly, append it to the unordered list.

Here’s the code responsible for this job:

1
const { main, name, sys, weather } = data;
2
const icon = `https://openweathermap.org/img/wn/${
3
  weather[0]["icon"]
4
}@2x.png`;
5

6
const li = document.createElement("li");
7
li.classList.add("city");
8
const markup = `
9
  <h2 class="city-name" data-name="${name},${sys.country}">
10
    <span>${name}</span>
11
    <sup>${sys.country}</sup>
12
  </h2>
13
  <div class="city-temp">${Math.round(main.temp)}<sup>°C</sup>
14
  </div>
15
  <figure>
16
    <img class="city-icon" src=${icon} alt=${weather[0]["main"]}>
17
    <figcaption>${weather[0]["description"]}</figcaption>
18
  </figure>
19
`;
20
li.innerHTML = markup;
21
list.appendChild(li);

There are two things here we have to discuss:

  1. If you look again at the response visualization above, you’ll notice that the API returns an icon code (e.g. “50d”) which holds the current weather condition for the target city. Based on this code, we’re able to construct the icon URL and display it in the card via the img tag.
  2. Inside the .city-name element of each list item, we’ll append the data-name attribute with value the cityName,countryCode (e.g. madrid,es). Later we’ll use this value to prevent duplicate requests.

Reset Things

Lastly, after the AJAX request, we’ll clear the content of the .msg element, the value of the search field, and give focus to that field as well:

1
...
2

3
msg.textContent = "";
4
form.reset();
5
input.focus();

Great job, folks! We’ve just created the first version of our weather app. By the time you put your own API key and search for a city, you should see a card layout similar to that one:

The unordered list layout with the default iconsThe unordered list layout with the default iconsThe unordered list layout with the default icons

Here’s the related CodePen demo:

Add Custom Weather Icons

Let’s now customize a little bit the look and feel of our app. We’ll replace the default OpenWeatherMap PNG icons with the SVGs we downloaded earlier from Envato Elements.

To do this, I’ve uploaded all the new icons to CodePen (via the Asset Manager as I’m a PRO member) and changed their names, so they will match the names and the weather conditions of the original icons, like this:

Mapping the custom icons with the original onesMapping the custom icons with the original onesMapping the custom icons with the original ones

Then, in the code, we only have to change the icon path:

1
 //BEFORE
2
const icon = `https://openweathermap.org/img/wn/${
3
  weather[0]["icon"]
4
}@2x.png`;
5

6
//AFTER
7
const icon = `https://s3-us-west-2.amazonaws.com/s.cdpn.io/162656/${
8
  weather[0]["icon"]
9
}.svg`;

Prevent Duplicate Requests

There’s still one thing we have to fix. So far, as we perform a successful AJAX request, a list item is created. That said, the list can contain multiple identical list items which refer to the same city, like so:

Identical list itemsIdentical list itemsIdentical list items

That’s bad user experience, so let’s make sure that only a single request is triggered for a specific city. 

But before that, there’s another thing for taking into consideration. The same city name can exist in more than one country. For example, if we search for “Athens” in the OpenWeatherMap’s search finder, we’ll see these results:

Cities that share the same nameCities that share the same nameCities that share the same name

With all the above in mind, we’ll write some code which will ensure that only a single request per city, per country will be executed:

1
...
2

3
//1
4
const listItems = list.querySelectorAll(".ajax-section .city");
5
const listItemsArray = Array.from(listItems);
6

7
if (listItemsArray.length > 0) {
8
  //2
9
  const filteredArray = listItemsArray.filter(el => {
10
    let content = "";
11
    //athens,gr
12
    if (inputVal.includes(",")) {
13
      //athens,grrrrrr->invalid country code, so we keep only the first part of inputVal
14
      if (inputVal.split(",")[1].length > 2) {
15
        inputVal = inputVal.split(",")[0];
16
        content = el.querySelector(".city-name span").textContent.toLowerCase();
17
      } else {
18
        content = el.querySelector(".city-name").dataset.name.toLowerCase();
19
      }
20
    } else {
21
      //athens
22
      content = el.querySelector(".city-name span").textContent.toLowerCase();
23
    }
24
    return content == inputVal.toLowerCase();
25
  });
26
  
27
  //3
28
  if (filteredArray.length > 0) {
29
    msg.textContent = `You already know the weather for ${
30
      filteredArray[0].querySelector(".city-name span").textContent
31
    } ...otherwise be more specific by providing the country code as well 😉`;
32
    form.reset();
33
    input.focus();
34
    return;
35
  }
36
}

Let me explain what actions happen here:

  1. Again during the submit handler, before making an AJAX request, we check to see whether the unordered list is empty or not. If it isn’t empty, that means at least one successful AJAX request has already been executed. 
  2. Next, we check to see if there’s a list item who’s the city name or the value of its data-name attribute are equal to the search field’s value.
  3. If so, that means the user already knows the weather for this city, so there’s no need to perform another AJAX request. As the following actions, we’ll show them a related message, clear the value of the search field and give it focus.

Note #1: As I’ve noticed, in case you search for a city with at most two-letters which don’t represent any country code (e.g. athens,aa), the API won’t return anything. On the other hand, if you search for a city along with at least three-letters which also don’t represent any country code (e.g. athens,aaaa), the API will ignore the part after the comma and return all cities named as the first part (e.g. athens).

Note #2: For this exercise, we won’t also cover the special case where a country contains more than one city with the same name (e.g. Athens in USA). So, for example, if a user searches for “athens,us” only one city will appear in the screen and not more. In order to cover the ideal scenario, users should somehow know the city ID (e.g. perhaps make them available as a dropdown) and search based on that instead of searching based on its name.

Excellent job, folks! We’ve just built our app. Let’s take a look:

Your Weather App Is Ready!

And we’re done! This really was quite a long journey, but I hope that you enjoyed it and that it has helped enhance your front-end skills.

Once again, don’t forget to put your own key for live app testing! 

As a reminder, let’s look again at how the weather app works:


As always, thanks a lot for reading!

Next Steps

There are so many things that you can do to extend the functionality of this weather app. Here are some thoughts:

  • Use geolocation to grab the user’s location, and then perform an AJAX request for retrieving weather data for their closest cities.
  • Use localStorage to persist the data above or even a real-time database like Firebase.
  • Use a charting library like Highcharts.js for building a meteogram that will give a weather forecast. If you do so, this tutorial might help.
  • Use an image API like Flickr API to present as a gallery lightbox a list of photos for each city.

If there’s anything else that you might want to see as an app extension, let me know in the comments below!