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}${data.current_units.temperature_2m}`;
      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 = `

${dayName}

${data.daily.time[index]} ${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)">

${minTemp}${minTempUnit}- ${maxTemp}${maxTempUnit} ${wind}km/h ${humidity}%

`
} }) changeWeatherBackground(data.current.weathercode); updateGraphs(data.current.wind_speed_10m, data.current.relative_humidity_2m); locationBtn.innerHTML = '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}${unit}`;
  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




Source link

Leave a Reply

Your email address will not be published. Required fields are marked *