DEV Community

Cover image for Build a Live Flight Radar in a Single HTML File
Sergey St
Sergey St

Posted on

Build a Live Flight Radar in a Single HTML File

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> &nbsp;|&nbsp;
  Updated: <b id="time"></b> &nbsp;|&nbsp;
  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: '&copy; OpenStreetMap &copy; 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 &nbsp; → ${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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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`;
Enter fullscreen mode Exit fullscreen mode

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.html as index.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)