When ISS passes overhead at the right moment, you can step outside and watch a bright point of light streak from west to east at ~7 km/s — apparent magnitude around −5, brighter than Venus, no telescope needed. The interesting part of building a "is it visible right now?" tool is that just plotting the dot on a map doesn't answer the question. Three independent conditions all have to be true:
- ISS above your horizon (geometry)
- ISS sunlit (not in Earth's shadow)
- Your sky dark enough (sun far enough below your horizon)
All three fall out of one REST API call plus ~200 lines of spherical trig. Here's the math.
🛰️ Demo: https://sen.ltd/portfolio/iss-tracker/
📦 GitHub: https://github.com/sen-ltd/iss-tracker
The API: wheretheiss.at gives you everything you need in one shot
There are several ISS position APIs. The one that's HTTPS-friendly, no-auth, and returns all the fields the visibility math needs is api.wheretheiss.at:
$ curl -s https://api.wheretheiss.at/v1/satellites/25544 | jq
{
"name": "iss",
"id": 25544,
"latitude": 28.857,
"longitude": -6.671,
"altitude": 416.42, # km
"velocity": 27603.81, # km/h
"visibility": "eclipsed", # ← is the satellite sunlit or in shadow?
"footprint": 4489.21, # km
"solar_lat": 15.30, # ← sub-solar point (where the sun is overhead)
"solar_lon": 197.78,
"timestamp": 1777675555
}
Two fields are gold:
-
visibilityis a satellite-side property — the API does the Earth-shadow geometry for you."daylight"means ISS is in sunlight;"eclipsed"means it's in Earth's shadow. That's condition 2 done. -
solar_lat,solar_lonis the sub-solar point — the (lat, lon) where the sun is directly overhead at this instant. With the observer's lat/lon, the sun's altitude above the observer's horizon is one trig formula away. That's the input for condition 3.
The older api.open-notify.org is HTTPS-broken and doesn't even return altitude. Use wheretheiss.
Condition 1: ISS above the observer's horizon
Treat Earth as a sphere of radius R. Given satellite altitude h and the great-circle distance d from observer to ISS sub-point, the slant range and elevation come from the OES triangle (Observer, Earth-centre, Satellite) via the law of cosines:
slant² = R² + (R+h)² − 2R(R+h)·cos(d/R)
sin(elevation) = ((R+h)·cos(d/R) − R) / slant
elevation > 0 means ISS is above the observer's horizon — physically possible to see. Negative means it's on the far side of the planet, no chance.
export function elevationDeg(observerLat, observerLon, subLat, subLon, altitudeKm) {
const d = haversineKm(observerLat, observerLon, subLat, subLon);
const θ = d / EARTH_RADIUS_KM;
const R = EARTH_RADIUS_KM, h = altitudeKm;
const slantSq = R*R + (R+h)**2 - 2*R*(R+h)*Math.cos(θ);
const sinE = ((R+h)*Math.cos(θ) - R) / Math.sqrt(slantSq);
return Math.asin(Math.max(-1, Math.min(1, sinE))) * 180 / Math.PI;
}
The footprint radius drops out
The locus of points where elevation = 0 (line-of-sight tangent to the Earth at the observer's feet) is a small circle on the sphere — the satellite's "footprint." Geometry:
cos α = R / (R + h)
footprint_km = R · α
For ISS at 408 km, α ≈ 20° and the footprint radius is ~2222 km. Anyone outside that radius from the sub-point is below the horizon, full stop.
export function footprintKm(altitudeKm) {
const cosAlpha = EARTH_RADIUS_KM / (EARTH_RADIUS_KM + altitudeKm);
return EARTH_RADIUS_KM * Math.acos(cosAlpha);
}
Condition 2: ISS in sunlight, not Earth's shadow
Even if ISS is directly overhead at midnight, if it's in Earth's shadow it's invisible. It has no light source. ISS reflects sunlight off its solar panels — that's why it's bright when sunlit.
ISS orbits Earth every ~90 minutes, so each orbit it passes through Earth's shadow for some fraction of the loop. That "ISS overhead but eclipsed" case is exactly why a midnight pass over your head can fail to be visible.
The API handles this for you: iss.visibility === "daylight" ⇒ sunlit ⇒ condition 2 met.
Condition 3: the observer's sky is dark enough
Daytime ISS isn't visible — the bright sky drowns out a 5th-magnitude moving point. We need the observer to be in twilight or darker.
Sun altitude from sub-solar point is one formula:
sin(sun_altitude) = sin(obs_lat) · sin(sun_lat)
+ cos(obs_lat) · cos(sun_lat) · cos(obs_lon − sun_lon)
Astronomical convention:
| Sun altitude | Phase | ISS visible? |
|---|---|---|
| > 0° | Day | No |
| 0° to −6° | Civil twilight | Sky still too bright |
| −6° to −12° | Nautical twilight | Marginal |
| −12° to −18° | Astronomical twilight | Yes |
| < −18° | Full night | Best |
The practical threshold for ISS visibility is sun altitude < −6° (civil twilight or darker).
export function sunAltitudeDeg(observerLat, observerLon, sunLat, sunLon) {
const φo = deg2rad(observerLat);
const φs = deg2rad(sunLat);
const dλ = deg2rad(observerLon - sunLon);
const sinH = Math.sin(φo)*Math.sin(φs) + Math.cos(φo)*Math.cos(φs)*Math.cos(dλ);
return rad2deg(Math.asin(Math.max(-1, Math.min(1, sinH))));
}
The combined verdict:
const visible = elevation > 0
&& iss.visibility === "daylight"
&& sunAltitudeDeg(obs.lat, obs.lon, iss.solar_lat, iss.solar_lon) < -6;
What this tool deliberately doesn't do
heavens-above.com and NASA's spotthestation tell you "ISS will pass over your location at 19:42 UTC tomorrow, max elevation 67°, visible from 19:43 to 19:47." To do that yourself you need:
- Two-Line Element sets (TLEs) from celestrak.com
- SGP4 orbital propagator — the standard NORAD model for satellites in low Earth orbit, ~2000 lines of dense numerical code (atmospheric drag, J2/J3/J4 perturbations, geopotential)
- A sweep over the next 24-48 hours sampling at minute resolution
- Apply the three-condition visibility check at each sample to find the windows
satellite.js is ~50 KB of bundled SGP4 if you want it. But spotthestation already does this perfectly, so this tool stays focused on right now: 200 lines, no library, no orbital propagator.
Map rendering — no coastlines, on purpose
The expected ISS-tracker visual is "moving icon on a Mercator world map." Shipping coastline data costs ~50-100 KB of bundled GeoJSON and brings projection-distortion problems near the poles. Equirectangular x = lon, y = -lat is one line of code; a graticule (lat/lon grid) gives the eye anchor points; the data overlay (ISS dot + footprint cap + observer + sub-solar) carries the visual weight. Cleaner, no dependency, fits the "the math is the show" angle.
<svg viewBox="-180 -90 360 180">
<g id="graticule"></g>
<g id="footprint"></g>
<g id="iss"></g>
<g id="observer"></g>
<g id="sun"></g>
</svg>
Takeaways
- ISS visibility = AND of three independent conditions: above-horizon (geometry), sunlit (API field), observer-sky-dark (sun altitude < −6°).
-
api.wheretheiss.atis the right API — HTTPS, no auth, returns altitude / velocity / visibility / sub-solar point in one call. - The geometry: haversine distance, OES triangle for elevation,
cos α = R/(R+h)for footprint,sin·sin + cos·cos·cosfor sun altitude. - Pass prediction (next time ISS will be visible) deliberately out of scope — that needs TLEs and an SGP4 propagator. NASA's spotthestation already nails it.
Full source on GitHub — iss.js is the 200-line geometry module, tests/iss.test.js is 24 cases. MIT.
Live demo updates every 5 s. The "Use geolocation" button pre-fills the observer to your current location.

Top comments (2)
Very cool👍️. Are you using Github to host your demo?
Some comments may only be visible to logged-in visitors. Sign in to view all comments.