SunCalc: sun and moon positions in JavaScript without a dependency tree
A tiny JavaScript library for calculating sun/moon positions and phases.
At a glance
- What is it?
- SunCalc is a dependency-free JavaScript library that returns sun altitude and azimuth, sunlight phases, moon position and lunar illumination for any date and coordinate. It is accurate enough for scheduling and display work, and it is not an ephemeris engine.
- Who is it for?
- Adopt SunCalc if you need sunrise, sunset, twilight, golden hour or moon illumination values inside a JavaScript or TypeScript application and you want them from one file with no runtime dependencies. Do not adopt it if you need sub-arcsecond ephemerides, planetary positions, or an almanac that accounts for terrain and buildings.
- Can I use it commercially?
- Yes. BSD-2-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 29 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What SunCalc computes, and the projects it fits
SunCalc answers a narrow question well: given a date, a latitude and a longitude, where are the sun and moon, and when does the sun cross a set of altitude thresholds. The README lists the surface area as sun position, sunlight phases (sunrise, sunset, dusk and the rest), moon position, moonrise and moonset times, and lunar phase. Package.json describes the same scope in one line and declares no runtime dependencies at all, only eslint, eslint-config-mourner and rolldown as devDependencies.
That combination defines the audience. It suits a web or Node application that needs to decide whether it is dark yet, when to switch a theme, when a scheduled job should run, or how full the moon will look in a header graphic. It suits anyone who would otherwise pull in an astronomy package and then use three functions from it. The version 2 line ships an index.d.ts, so a TypeScript project gets types without a separate @types package.
The library is not a general astronomy toolkit. The README names its scope as sun and moon only, and the accuracy claim is a comparison rather than a tolerance: it states that results match the accuracy and conventions of timeanddate.com and the U.S. Naval Observatory, with calculations based on the formulas in Jean Meeus' Astronomical Algorithms. No error figure in degrees or seconds is given anywhere in the README, so treat the comparison as the claim and measure against your own reference data if the margin matters.
The API surface and how the calculations are structured
There are five functions in the reference, and they split into two kinds: instantaneous positions and threshold crossings.
SunCalc.getPosition(date, lat, lng) returns altitude and azimuth. Altitude is described as apparent and refraction-corrected, in degrees, zero at the horizon and 90 at the zenith. Azimuth is degrees clockwise from north, so 0 is north, 90 east, 180 south, 270 west. SunCalc.getMoonPosition(date, lat, lng) returns altitude and azimuth on the same convention plus distance in kilometers and the parallactic angle in degrees.
SunCalc.getTimes(date, lat, lng, height = 0) is the interesting one. It takes an optional observer height in meters above the horizon and returns an object of Date values for the solar day containing the given date. The README lists fourteen properties in the order they occur, from nadir through nightEnd, nauticalDawn, dawn, sunrise, sunriseEnd, goldenHourEnd, solarNoon, goldenHour, sunsetStart, sunset, dusk, nauticalDusk and night. Each is a threshold crossing on the sun's altitude, which is why the same function can express civil, nautical and astronomical twilight without separate calls. solarNoon and nadir are always present.
The fourth piece is extensibility. SunCalc.addTime(angleInDegrees, morningName, eveningName) registers a custom altitude crossing, with the morning name used for the ascending pass and the evening name for the descending one. SunCalc.times exposes the current table as rows of angle, morning name and evening name. The README's example adds the blue hour at minus 4 and minus 8 degrees.
The fifth is SunCalc.getMoonIllumination(date), which returns fraction, phase, angle and waxing. The README is unusually careful about fraction: it varies from 0.0 at new moon to 1.0 at full moon, but reaches the exact extremes only at perfect syzygy, so a full moon typically peaks slightly under 1.0. That detail matters if you compare the value against 1.0 with strict equality.
Installing SunCalc and getting your first sunrise time
The README gives npm as the install path. The package is ESM-first: package.json sets "type": "module" and maps the import condition to index.js, while require resolves to a CommonJS bundle at suncalc.cjs. Both files are listed in the files array, so both ship.
npm install suncalcAfter that, import the namespace and call getTimes with a date and coordinates. The README's own example uses London at 51.5, -0.1 and logs the sunrise time and the sun's azimuth at that moment.
import * as SunCalc from 'suncalc';
const times = SunCalc.getTimes(new Date(), 51.5, -0.1);
console.log(`Sunrise time: ${times.sunrise.toLocaleString()}`);
const sunrisePos = SunCalc.getPosition(times.sunrise, 51.5, -0.1);
console.log(`Sunrise direction: ${sunrisePos.azimuth} degrees`);You should see a localized sunrise timestamp and a direction in degrees. The README is explicit that the returned Dates are absolute UTC instants with no time zone of their own, and that toLocaleString without a timeZone argument uses the machine's local zone, so the same code prints different clock times on different machines. To pin the output, pass the zone explicitly.
times.sunset.toLocaleString('en-GB', {timeZone: 'Europe/Kyiv'});The README states this produces "24/08/2026, 19:59:00" for its Kyiv example, and that daylight saving time is applied for you when a timeZone is given. For a browser without a build step, the README offers two script tags: an ES module import from the jsDelivr ESM endpoint, or a bundle that exposes a SunCalc global.
<script src="https://cdn.jsdelivr.net/npm/suncalc"></script>Adding a custom phase is a one-liner, and the README's blue hour example is the clearest demonstration of the angle model: each call registers an angle and two names.
SunCalc.addTime(-4, 'morningBlueHourEnd', 'blueHour');
SunCalc.addTime(-8, 'morningBlueHour', 'blueHourEnd');Polar latitudes, null times and the failure mode to plan for
The most consequential behaviour in the README is what happens when an event does not occur. At high latitudes, when the sun stays above or below the rise and set altitude for the whole day, the rise and set related times are null, and one of two flags is set: alwaysUp when the sun never sets that day, and alwaysDown when it never rises. solarNoon and nadir are always present.
This is a design decision with real consequences. Any code that formats times.sunrise unconditionally will throw or print "Invalid Date" during a polar summer or winter, and the failure will be seasonal, which means it can survive testing and appear months later in production. The README documents the flags but does not document a helper for them, so the check is yours to write. The same applies to the moon: the README does not list an equivalent alwaysUp or alwaysDown flag on the moon functions, and it does not document moonrise and moonset as separate named functions in the reference section at all, even though the description mentions those times. That is a gap between the description and the reference, and you should confirm the exact call you need against index.d.ts or the source before designing around it.
The second limitation is scope. SunCalc has no terrain, no buildings, no atmospheric model beyond the refraction correction on altitude, and no planetary bodies. If your product needs a shadow study for a specific rooftop or an almanac for an observatory, the README's own framing points you elsewhere: it cites Meeus' Astronomical Algorithms as the source of the formulas, and that book is the reference for building a fuller model. SunCalc is the condensed version of it, not a replacement.
SunCalc against a full ephemeris library
The honest alternative is an astronomy library that covers the whole solar system, such as astronomy-engine or a similar package that computes positions for planets, stars and satellites alongside the sun and moon. The difference is not accuracy in the abstract, it is what each one is built to answer.
SunCalc's inputs are a date, a latitude and a longitude, and its outputs are a small set of named values. There is no observer object, no coordinate frame to choose, no nutation or precession switch to set. A general ephemeris library exposes those concepts because its users need them: topocentric versus geocentric coordinates, apparent versus astrometric positions, and bodies beyond the moon. The cost is a larger API and, typically, a larger install.
For a sunrise countdown or a moon-phase icon, that extra surface is unused weight. For an application that plots the ecliptic or tracks a satellite pass, SunCalc simply does not have the functions, and no amount of configuration will add them. The decision is therefore about output shape, not about which library is better: if your required outputs are in SunCalc's reference list, it covers them; if they are not, it will not.
There is a middle option worth noting from the README itself. Because addTime registers arbitrary altitude thresholds, some needs that look like missing features are expressible as angles. The blue hour example is exactly that. If your requirement is a named sun altitude, you can add it; if your requirement is another celestial body, you cannot.
Upgrades, licence and the cost of keeping it current
The repository is not archived, and the last push was on 2026-09-02. Releases are recent and close together: v2.0.0 on 2026-06-18, v2.0.1 on 2026-07-11 and v2.0.2 on 2026-09-02. The 2.x line is the one to target, and the package.json shows a modern setup with ESM plus a CommonJS fallback, generated types and a rolldown build. The publish pipeline is visible in the scripts: pretest runs eslint, test runs node --test over test/*.test.js, and prepublishOnly runs the build.
For a consumer, the upgrade cost is low but not zero. The library has no runtime dependencies, so there is no transitive surface to audit and no version conflict to resolve. The main breaking-change risk in a 2.x line is the module format and the types, both of which are declared explicitly in the exports map. If you consume the package from CommonJS, you are on suncalc.cjs; if you import it, you are on index.js. A bundler that ignores the exports field may resolve the wrong entry, which is worth checking once rather than debugging later.
The licence is BSD-2-Clause, declared in the repository's LICENSE file. That is a permissive licence, and it is the kind that generally allows use in closed-source products provided the copyright notice and licence text are retained. SunCalc is a library, not a service, so there is no network boundary to think about. This is a description of what the repository declares, not legal advice; if your organisation has a licence review process, the file to hand over is LICENSE at the repository root.
Editorial conclusion
Adopt SunCalc if you need sunrise, sunset, twilight, golden hour or moon illumination values inside a JavaScript or TypeScript application and you want them from one file with no runtime dependencies. Do not adopt it if you need sub-arcsecond ephemerides, planetary positions, or an almanac that accounts for terrain and buildings. Before you ship, verify two things yourself: that the returned times are what you expect for a polar latitude, where the README says rise and set values come back as null with an alwaysUp or alwaysDown flag, and what your own runtime prints for a Date, since the library returns absolute instants and the clock time depends on the time zone you pass to toLocaleString.
Frequently asked questions
Is SunCalc free?
Yes. The repository is licensed under BSD-2-Clause, and the package is published to npm as suncalc, so you install it with npm install suncalc and use it under that licence.
Is SunCalc legit?
The library is published on npm as suncalc and its source lives in the mourner/suncalc repository, with a BSD-2-Clause LICENSE file at the root. The README states that its results match the accuracy and conventions of timeanddate.com and the U.S. Naval Observatory.
How accurate is SunCalc?
The README states that SunCalc matches the accuracy and conventions of timeanddate.com and the U.S. Naval Observatory, and that the calculations are based on the formulas in Jean Meeus' Astronomical Algorithms. It does not publish a numeric error margin, so the comparison is the claim rather than a tolerance.
How do I use SunCalc?
Install it with npm install suncalc, import it as a module, then call SunCalc.getTimes(date, lat, lng) for sunlight phases or SunCalc.getPosition(date, lat, lng) for the sun's altitude and azimuth. The returned times are absolute instants, so pass an explicit timeZone to toLocaleString if you want a fixed clock time.
How does SunCalc work?
It converts a date and a latitude and longitude into sun and moon positions and into the times the sun crosses a table of altitude angles. SunCalc.addTime lets you register additional angles, and SunCalc.times exposes the current table.
Is it still light 20 minutes after sunset?
SunCalc does not answer that directly, but it gives you the pieces: getTimes returns sunset, dusk and the twilight crossings as separate Date values, so you can compare your own offset against them. The README points to the Wikipedia twilight article for what each phase means.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/mourner-suncalc)