One HTML file. No npm, no frameworks, no backend. Open it in a browser and watch live aircraft move across an interactive map.
We'll use Leaflet.js for the map and a free aviation API for real-time ADS-B positions. The whole thing is under 100 lines of code, and the planes update every 30 seconds.
What We're Building
An interactive radar-style map that:
- Shows live aircraft positions over any region you choose
- Rotates plane icons to match actual heading
- Color-codes planes by altitude (green → yellow → red)
- Displays flight number, route, speed and altitude on click
- Auto-refreshes every 30 seconds without reloading the page
Before You Start
Grab a free API key from AirLabs — takes 30 seconds, no credit card. Their free tier returns real-time lat/lng, heading, altitude, speed, and route data, which is everything we need for a map.
The Full Code
I'll show the complete file first, then walk through how it works. Create a file called radar.html, paste this in, replace YOUR_API_KEY, and open it in your browser:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Live Flight Radar</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.9/dist/leaflet.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a1a; font-family: monospace; }
#map { width: 100vw; height: 100vh; }
#hud {
position: fixed; top: 16px; left: 16px; z-index: 999;
background: rgba(10,10,26,0.85); color: #8cf;
padding: 10px 16px; border-radius: 8px;
font-size: 13px; pointer-events: none;
border: 1px solid rgba(100,180,255,0.2);
}
#hud b { color: #fff; }
</style>
</head>
<body>
<div id="hud">
Flights: <b id="count">—</b> |
Updated: <b id="time">—</b> |
Next refresh: <b id="tick">30</b>s
</div>
<div id="map"></div>
<script>
const API_KEY = "YOUR_API_KEY";
const BBOX = [44, 2, 52, 20]; // Central Europe — change to your region
const REFRESH = 30; // seconds between updates
// --- MAP SETUP ---
const map = L.map("map", {
center: [(BBOX[0]+BBOX[2])/2, (BBOX[1]+BBOX[3])/2],
zoom: 6,
zoomControl: false
});
L.tileLayer("https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", {
attribution: '© OpenStreetMap © CARTO',
maxZoom: 18
}).addTo(map);
L.control.zoom({ position: "bottomright" }).addTo(map);
// --- PLANE ICON FACTORY ---
function planeIcon(dir, alt) {
let color;
if (alt == null) color = "#888";
else if (alt < 3000) color = "#2ecc71";
else if (alt < 8000) color = "#f1c40f";
else color = "#e74c3c";
return L.divIcon({
className: "",
iconSize: [22, 22],
iconAnchor: [11, 11],
html: `<div style="
font-size:18px; color:${color};
transform:rotate(${dir || 0}deg);
text-shadow:0 0 4px ${color}44;
line-height:22px; text-align:center;
">✈</div>`
});
}
// --- POPUP ---
function popupHTML(f) {
const flight = f.flight_iata || f.hex || "—";
const route = `${f.dep_iata || "?"} → ${f.arr_iata || "?"}`;
const altFt = f.alt ? Math.round(f.alt * 3.281).toLocaleString() : "—";
const kts = f.speed ? Math.round(f.speed * 0.54) : "—";
return `<div style="font-family:monospace;font-size:13px;min-width:150px">
<b style="font-size:15px">${flight}</b><br>
${route}<br>
✈ ${f.aircraft_icao || "—"}<br>
↑ ${altFt} ft → ${kts} kts
</div>`;
}
// --- STATE ---
let markers = [];
async function fetchFlights() {
const url = `https://airlabs.co/api/v9/flights`
+ `?api_key=${API_KEY}`
+ `&bbox=${BBOX.join(",")}`
+ `&_fields=hex,flag,lat,lng,dir,alt,speed,flight_iata,dep_iata,arr_iata,aircraft_icao`;
try {
const res = await fetch(url);
const json = await res.json();
const flights = json.response || [];
// clear old markers
markers.forEach(m => map.removeLayer(m));
markers = [];
flights.forEach(f => {
if (!f.lat || !f.lng) return;
const m = L.marker([f.lat, f.lng], {
icon: planeIcon(f.dir, f.alt)
}).bindPopup(popupHTML(f));
m.addTo(map);
markers.push(m);
});
// update HUD
document.getElementById("count").textContent = flights.length;
document.getElementById("time").textContent =
new Date().toLocaleTimeString();
} catch (err) {
console.error("Fetch failed:", err);
}
}
// --- REFRESH LOOP ---
let countdown = REFRESH;
fetchFlights(); // first load
setInterval(() => {
countdown--;
document.getElementById("tick").textContent = countdown;
if (countdown <= 0) {
countdown = REFRESH;
fetchFlights();
}
}, 1000);
</script>
</body>
</html>
That's the entire thing. Let's break it down.
How It Works
The Map
We initialize Leaflet with CARTO's dark basemap — it gives that clean radar aesthetic, and it's free. The map centers automatically on whatever bounding box you configure.
const BBOX = [44, 2, 52, 20]; // south-lat, west-lng, north-lat, east-lng
Change this to any region you want:
-
USA (lower 48):
[24, -125, 50, -66] -
Southeast Asia:
[-10, 95, 25, 145] -
UK + Ireland:
[49, -11, 60, 2] -
Middle East:
[12, 32, 42, 62] - Your city: grab coordinates from Google Maps, add ±2 degrees
The API Call
One fetch() to the AirLabs /flights endpoint with a bounding box filter:
const url = `https://airlabs.co/api/v9/flights`
+ `?api_key=${API_KEY}`
+ `&bbox=${BBOX.join(",")}`
+ `&_fields=hex,flag,lat,lng,dir,alt,speed,flight_iata,dep_iata,arr_iata,aircraft_icao`;
The _fieldsparameter limits the response to only what we need — keeps it fast. A typical European bounding box returns 500–1500 flights depending on time of day.
Plane Icons
Each plane is a Leaflet DivIcon — a rotated ✈ character. The dir field from the API gives heading in degrees, so we apply a CSS rotate() transform. Color is based on altitude:
| Altitude | Color | Meaning |
|---|---|---|
| < 3,000m | 🟢 Green | Climbing or descending |
| 3,000–8,000m | 🟡 Yellow | Mid-altitude |
| > 8,000m | 🔴 Red | Cruising altitude |
Auto-Refresh
A simple setIntervalcounts down from 30 seconds. When it hits zero, it calls fetchFlights() again — clears old markers, fetches new positions, plots them. The HUD in the top-left shows the flight count, last update time, and countdown to next refresh.
No WebSockets, no server, no state management. Just clear and redraw.
Customization Ideas
Filter by airline — add &airline_iata=UA to the URL to show only United Airlines flights. Great for monitoring a specific carrier.
Departure board — swap /flights for /schedules?dep_iata=JFK to build a live airport departure board. Returns gate, terminal, delay, and status.
Draw flight paths — when user clicks a plane, fetch the route with /routes?dep_iata=${dep}&arr_iata=${arr} and draw a great-circle arc between the airports using Leaflet's polyline.
Add airports — fetch /airports?_fields=iata_code,name,lat,lng and plot them as circles on the map. Now your radar has reference points.
Mobile PWA — wrap this HTML in a simple service worker and add a manifest. You've got a progressive web app that works offline (minus the live data) and installs on your home screen.
Deployment
This is a single HTML file with zero dependencies (Leaflet loads from CDN). You can:
-
GitHub Pages — push
radar.htmlasindex.html, enable Pages, done - Netlify / Vercel — drag and drop the file
- Literally any web server — it's static HTML
One caveat: the API key is visible in client-side code. For a personal project or portfolio demo, this is fine — just use the free tier key and don't worry about it. For production, proxy the request through a small serverless function (Cloudflare Workers or Vercel Edge work great for this).
The entire project is 93 lines of HTML/CSS/JS. No build step, no node_modules, no dependencies to update. Just a file that shows you where every plane in the sky is, right now.
Top comments (0)