cd /news/developer-tools/build-a-live-flight-radar-in-a-singl… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-10119] src=dev.to β†— pub= topic=developer-tools verified=true sentiment=↑ positive

Build a Live Flight Radar in a Single HTML File

This article explains how to create a live flight radar using a single HTML file that requires no npm, frameworks, or backend. It uses Leaflet.js for the interactive map and a free AirLabs API to display real-time ADS-B aircraft positions, updating every 30 seconds. The complete code is under 100 lines and only requires replacing a placeholder API key to function.

read6 min views20 publishedMay 22, 2026

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 re 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>

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 _fields

parameter 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 setInterval

counts 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β€” pushradar.html

asindex.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.

── more in #developer-tools 4 stories Β· sorted by recency
── more on @leaflet.js 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/build-a-live-flight-…] indexed:0 read:6min 2026-05-22 Β· β€”