{"slug": "build-a-live-flight-radar-in-a-single-html-file", "title": "Build a Live Flight Radar in a Single HTML File", "summary": "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.", "body_md": "One HTML file. No npm, no frameworks, no backend. Open it in a browser and watch live aircraft move across an interactive map.\n\nWe'll use [Leaflet.js](https://leafletjs.com/) 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.\n\n## What We're Building\n\nAn interactive radar-style map that:\n\n- Shows live aircraft positions over any region you choose\n- Rotates plane icons to match actual heading\n- Color-codes planes by altitude (green → yellow → red)\n- Displays flight number, route, speed and altitude on click\n- Auto-refreshes every 30 seconds without reloading the page\n\n## Before You Start\n\nGrab a free API key from [AirLabs](https://airlabs.co/) — 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.\n\n## The Full Code\n\nI'll show the complete file first, then walk through how it works. Create a file called `radar.html`\n\n, paste this in, replace `YOUR_API_KEY`\n\n, and open it in your browser:\n\n```\n<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>Live Flight Radar</title>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<link rel=\"stylesheet\" href=\"https://unpkg.com/leaflet@1.9/dist/leaflet.css\">\n<script src=\"https://unpkg.com/leaflet@1.9/dist/leaflet.js\"></script>\n<style>\n  * { margin: 0; padding: 0; box-sizing: border-box; }\n  body { background: #0a0a1a; font-family: monospace; }\n  #map { width: 100vw; height: 100vh; }\n  #hud {\n    position: fixed; top: 16px; left: 16px; z-index: 999;\n    background: rgba(10,10,26,0.85); color: #8cf;\n    padding: 10px 16px; border-radius: 8px;\n    font-size: 13px; pointer-events: none;\n    border: 1px solid rgba(100,180,255,0.2);\n  }\n  #hud b { color: #fff; }\n</style>\n</head>\n<body>\n<div id=\"hud\">\n  Flights: <b id=\"count\">—</b> &nbsp;|&nbsp;\n  Updated: <b id=\"time\">—</b> &nbsp;|&nbsp;\n  Next refresh: <b id=\"tick\">30</b>s\n</div>\n<div id=\"map\"></div>\n\n<script>\nconst API_KEY = \"YOUR_API_KEY\";\nconst BBOX    = [44, 2, 52, 20];      // Central Europe — change to your region\nconst REFRESH = 30;                    // seconds between updates\n\n// --- MAP SETUP ---\nconst map = L.map(\"map\", {\n  center: [(BBOX[0]+BBOX[2])/2, (BBOX[1]+BBOX[3])/2],\n  zoom: 6,\n  zoomControl: false\n});\n\nL.tileLayer(\"https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png\", {\n  attribution: '&copy; OpenStreetMap &copy; CARTO',\n  maxZoom: 18\n}).addTo(map);\n\nL.control.zoom({ position: \"bottomright\" }).addTo(map);\n\n// --- PLANE ICON FACTORY ---\nfunction planeIcon(dir, alt) {\n  let color;\n  if (alt == null)    color = \"#888\";\n  else if (alt < 3000) color = \"#2ecc71\";\n  else if (alt < 8000) color = \"#f1c40f\";\n  else                 color = \"#e74c3c\";\n\n  return L.divIcon({\n    className: \"\",\n    iconSize: [22, 22],\n    iconAnchor: [11, 11],\n    html: `<div style=\"\n      font-size:18px; color:${color};\n      transform:rotate(${dir || 0}deg);\n      text-shadow:0 0 4px ${color}44;\n      line-height:22px; text-align:center;\n    \">✈</div>`\n  });\n}\n\n// --- POPUP ---\nfunction popupHTML(f) {\n  const flight = f.flight_iata || f.hex || \"—\";\n  const route  = `${f.dep_iata || \"?\"} → ${f.arr_iata || \"?\"}`;\n  const altFt  = f.alt ? Math.round(f.alt * 3.281).toLocaleString() : \"—\";\n  const kts    = f.speed ? Math.round(f.speed * 0.54) : \"—\";\n  return `<div style=\"font-family:monospace;font-size:13px;min-width:150px\">\n    <b style=\"font-size:15px\">${flight}</b><br>\n    ${route}<br>\n    ✈ ${f.aircraft_icao || \"—\"}<br>\n    ↑ ${altFt} ft &nbsp; → ${kts} kts\n  </div>`;\n}\n\n// --- STATE ---\nlet markers = [];\n\nasync function fetchFlights() {\n  const url = `https://airlabs.co/api/v9/flights`\n    + `?api_key=${API_KEY}`\n    + `&bbox=${BBOX.join(\",\")}`\n    + `&_fields=hex,flag,lat,lng,dir,alt,speed,flight_iata,dep_iata,arr_iata,aircraft_icao`;\n\n  try {\n    const res = await fetch(url);\n    const json = await res.json();\n    const flights = json.response || [];\n\n    // clear old markers\n    markers.forEach(m => map.removeLayer(m));\n    markers = [];\n\n    flights.forEach(f => {\n      if (!f.lat || !f.lng) return;\n      const m = L.marker([f.lat, f.lng], {\n        icon: planeIcon(f.dir, f.alt)\n      }).bindPopup(popupHTML(f));\n      m.addTo(map);\n      markers.push(m);\n    });\n\n    // update HUD\n    document.getElementById(\"count\").textContent = flights.length;\n    document.getElementById(\"time\").textContent =\n      new Date().toLocaleTimeString();\n  } catch (err) {\n    console.error(\"Fetch failed:\", err);\n  }\n}\n\n// --- REFRESH LOOP ---\nlet countdown = REFRESH;\n\nfetchFlights();  // first load\n\nsetInterval(() => {\n  countdown--;\n  document.getElementById(\"tick\").textContent = countdown;\n  if (countdown <= 0) {\n    countdown = REFRESH;\n    fetchFlights();\n  }\n}, 1000);\n</script>\n</body>\n</html>\n```\n\nThat's the entire thing. Let's break it down.\n\n## How It Works\n\n### The Map\n\nWe 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.\n\n``` js\nconst BBOX = [44, 2, 52, 20]; // south-lat, west-lng, north-lat, east-lng\n```\n\nChange this to any region you want:\n\n-\n**USA (lower 48)**:`[24, -125, 50, -66]`\n\n-\n**Southeast Asia**:`[-10, 95, 25, 145]`\n\n-\n**UK + Ireland**:`[49, -11, 60, 2]`\n\n-\n**Middle East**:`[12, 32, 42, 62]`\n\n-\n**Your city**: grab coordinates from Google Maps, add ±2 degrees\n\n### The API Call\n\nOne `fetch()`\n\nto the AirLabs `/flights`\n\nendpoint with a bounding box filter:\n\n``` js\nconst url = `https://airlabs.co/api/v9/flights`\n  + `?api_key=${API_KEY}`\n  + `&bbox=${BBOX.join(\",\")}`\n  + `&_fields=hex,flag,lat,lng,dir,alt,speed,flight_iata,dep_iata,arr_iata,aircraft_icao`;\n```\n\nThe `_fields`\n\nparameter 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.\n\n### Plane Icons\n\nEach plane is a Leaflet `DivIcon`\n\n— a rotated ✈ character. The `dir`\n\nfield from the API gives heading in degrees, so we apply a CSS `rotate()`\n\ntransform. Color is based on altitude:\n\n| Altitude | Color | Meaning |\n|---|---|---|\n| < 3,000m | 🟢 Green | Climbing or descending |\n| 3,000–8,000m | 🟡 Yellow | Mid-altitude |\n| > 8,000m | 🔴 Red | Cruising altitude |\n\n### Auto-Refresh\n\nA simple `setInterval`\n\ncounts down from 30 seconds. When it hits zero, it calls `fetchFlights()`\n\nagain — 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.\n\nNo WebSockets, no server, no state management. Just clear and redraw.\n\n## Customization Ideas\n\n**Filter by airline** — add `&airline_iata=UA`\n\nto the URL to show only United Airlines flights. Great for monitoring a specific carrier.\n\n**Departure board** — swap `/flights`\n\nfor `/schedules?dep_iata=JFK`\n\nto build a live airport departure board. Returns gate, terminal, delay, and status.\n\n**Draw flight paths** — when user clicks a plane, fetch the route with `/routes?dep_iata=${dep}&arr_iata=${arr}`\n\nand draw a great-circle arc between the airports using Leaflet's polyline.\n\n**Add airports** — fetch `/airports?_fields=iata_code,name,lat,lng`\n\nand plot them as circles on the map. Now your radar has reference points.\n\n**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.\n\n## Deployment\n\nThis is a single HTML file with zero dependencies (Leaflet loads from CDN). You can:\n\n-\n**GitHub Pages**— push`radar.html`\n\nas`index.html`\n\n, enable Pages, done -\n**Netlify / Vercel**— drag and drop the file -\n**Literally any web server**— it's static HTML\n\nOne 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).\n\nThe entire project is 93 lines of HTML/CSS/JS. No build step, no `node_modules`\n\n, no dependencies to update. Just a file that shows you where every plane in the sky is, right now.", "url": "https://wpnews.pro/news/build-a-live-flight-radar-in-a-single-html-file", "canonical_source": "https://dev.to/sergeysta/build-a-live-flight-radar-in-a-single-html-file-p9p", "published_at": "2026-05-22 21:35:43+00:00", "updated_at": "2026-05-22 22:02:09.095605+00:00", "lang": "en", "topics": ["developer-tools", "data", "products"], "entities": ["Leaflet.js", "AirLabs", "ADS-B"], "alternates": {"html": "https://wpnews.pro/news/build-a-live-flight-radar-in-a-single-html-file", "markdown": "https://wpnews.pro/news/build-a-live-flight-radar-in-a-single-html-file.md", "text": "https://wpnews.pro/news/build-a-live-flight-radar-in-a-single-html-file.txt", "jsonld": "https://wpnews.pro/news/build-a-live-flight-radar-in-a-single-html-file.jsonld"}}