DEV Community

Cover image for Build a Modern Weather Forecast App with JavaScript & Tailwind
Sufal Thakre
Sufal Thakre

Posted on

Build a Modern Weather Forecast App with JavaScript & Tailwind

A small, real-world project to practice APIs, responsive UI, and frontend polish.

Demo Video

I built a modern, responsive Weather Forecast App using Vanilla JavaScript and Tailwind CSS, powered by the Open-Meteo API. It supports city search, geolocation, a 5-day forecast, animated UI, a °C/°F toggle (today only), and recent searches saved to localStorage. This article shows why and how I built it, the core code, setup steps, and lessons learned.


Why build this

Small projects let you practice real problems: async API calls, data normalization, UI updates, responsive layout, and error handling. I wanted a project that looks polished and is practical — useful for a portfolio and for learning frontend fundamentals.


What it does (features)

  • Search weather by city name (geocoding)
  • Use browser geolocation to get local weather
  • Show current temperature, humidity, wind speed and weather description
  • 5-day forecast with icons, min/max temps, wind & humidity
  • °C / °F toggle (applies to today's temperature)
  • Recent searches saved to localStorage
  • Animated backgrounds (sunny, rainy, cloudy, stormy, snowy)
  • Small SVG area charts for wind & humidity
  • Graceful error handling with a custom popup (no alert())

Tech stack

  • JavaScript (ES6+)
  • Tailwind CSS
  • Open-Meteo API (no API key required)
  • Static HTML/CSS/JS (no build tools required)

How it works (data flow)

  1. User searches city → geocoding API returns latitude & longitude.
  2. Use lat/lon to fetch forecast and current weather from Open-Meteo.
  3. Normalize fields, update DOM with current and daily data.
  4. Save city to recent searches (localStorage).
  5. Handle errors, show popup, and keep UI responsive.

Key code snippets

Robust fetch with timeout & validation

async function fetchWeatherData(lat, lon, locationName) {

  let params = new URLSearchParams({
    latitude: lat,
    longitude: lon,
    current: 'temperature_2m,relative_humidity_2m,wind_speed_10m,weathercode',
    daily: 'temperature_2m_max,temperature_2m_min,weathercode,wind_speed_10m_max,relative_humidity_2m_mean',
    temperature_unit: 'celsius',
    timezone: 'auto',
    forecast_days: 5
  })

  try {
    const weather = await fetch(`${WEATHER_URL}?${params}`)
    if (!weather.ok) {
      throw new Error(`Weather API error: ${weather.status} ${weather.statusText}`);
    }
    const data = await weather.json();

    // Current weather section updation in UI
    let currentTemp = 0;
    const interval = setInterval(() => {
      currentTemp++;
      currentTemperatur.innerHTML = `${currentTemp}<span class="text-4xl md:text-6xl">${data.current_units.temperature_2m}</span>`;
      if (currentTemp >= Math.round(data.current.temperature_2m)) clearInterval(interval);
    }, 50);
    locationDisplay.textContent = locationName
    humidityLevel.textContent = `${data.current.relative_humidity_2m}%`
    windSpeed.textContent = `${data.current.wind_speed_10m}km/h`
    weatherCondition.textContent = `${weatherMap[data.current.weathercode].h1 || 'unknown'}`
    weatherDescription.textContent = `${weatherMap[data.current.weathercode].h2 || "No data available"}`
    weatherExplanation.textContent = `${weatherMap[data.current.weathercode].p || ""}`
    rawTemp = data.current.temperature_2m;


    // 5 days forecast updation
    forecastCard.forEach((cards, index) => {
      if (index < 5) {
        const minTemp = Math.round(data.daily.temperature_2m_min[index]);
        const minTempUnit = data.daily_units.temperature_2m_min
        const maxTemp = Math.round(data.daily.temperature_2m_max[index]);
        const maxTempUnit = data.daily_units.temperature_2m_max
        const wind = data.daily.wind_speed_10m_max[index];
        const humidity = data.daily.relative_humidity_2m_mean[index];
        const date = new Date(data.daily.time[index]);
        const dayName = date.toLocaleDateString('en-US', { weekday: 'long' });

        // weather code for cloude icons 
        const weathericon = getCloudIcon(data.daily.weathercode[index])
        cards.innerHTML = `<h3 class="text-xs md:text-sm text-white/60 mb-1">${dayName}</h3>
          <h2 class="text-xs mb-2 md:mb-3 text-white/50">${data.daily.time[index]}</h2>
          <i class="fas fa-${weathericon} text-3xl md:text-4xl lg:text-5xl my-2 md:my-3" style="text-shadow: 0 0 20px rgba(255,255,255,0.5)"></i>
          <div class="flex justify-around text-sm mt-2">
              <span><i class="fas fa-thermometer-half mr-1"></i>${minTemp}${minTempUnit}- ${maxTemp}${maxTempUnit}</span>
              <span><i class="fas fa-wind mr-1"></i>${wind}km/h</span>
              <span><i class="fas fa-droplet mr-1"></i>${humidity}%</span>
          </div>`
      }
    })

    changeWeatherBackground(data.current.weathercode);
    updateGraphs(data.current.wind_speed_10m, data.current.relative_humidity_2m);
    locationBtn.innerHTML = '<i class="fas fa-location-arrow mr-2"></i>Use My Location';

    // Extreme temp alerts
    if (Math.round(data.current.temperature_2m) > 40) showError('Extreme Heat Alert: Temperature above 40°C! Stay hydrated and avoid direct sun.');
    if (Math.round(data.current.temperature_2m) < 5) showError('Extreme Cold Alert: Temperature below 5°C! Dress warmly and limit outdoor exposure.');


  } catch (error) {
    console.error('Weather fetch failed:', error);
    showError('Failed to load weather. Check internet or try again later.');
  }

}
Enter fullscreen mode Exit fullscreen mode

Temperature toggle (today only)

// toggle button for c/f
let rawTemp = 0, unit = '°C';
document.getElementById('toggleUnit').onclick = () => {
  unit = unit === '°C' ? '°F' : '°C';
  const t = unit === '°C' ? Math.round(rawTemp) : Math.round(rawTemp * 9/5 + 32);
  currentTemperatur.innerHTML = `${t}<span class="text-4xl md:text-6xl">${unit}</span>`;
  this.textContent = unit;
};
Enter fullscreen mode Exit fullscreen mode

Challenges & lessons

  • API fields sometimes vary — defensive data normalization was needed.
  • Animations caused repaint issues on low-end devices — solved with layers and reduced blur.
  • Geolocation permission errors must be handled gracefully for a good UX.

Links


Top comments (0)