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)
- User searches city → geocoding API returns latitude & longitude.
- Use lat/lon to fetch forecast and current weather from Open-Meteo.
- Normalize fields, update DOM with current and daily data.
- Save city to recent searches (localStorage).
- 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.');
}
}
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;
};
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.

Top comments (0)