# Build a Live Flight Radar in a Single HTML File

> Source: <https://dev.to/sergeysta/build-a-live-flight-radar-in-a-single-html-file-p9p>
> Published: 2026-05-22 21:35:43+00:00

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](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.

## 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](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.

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

``` js
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:

``` js
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**— 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.
