How I Built a Dark Mode Firefox Extension That Schedules by Sunset: NightOwl Technical Walkthrough
I wanted a dark mode extension that automatically scheduled itself by sunset and sunrise. Dark Reader (the dominant dark mode extension) does not have this feature. So I built NightOwl. This is the technical approach I
I wanted a dark mode extension that automatically scheduled itself by sunset and sunrise. Dark Reader (the dominant dark mode extension) does not have this feature. So I built NightOwl.
This is the technical approach I used for the Firefox WebExtension.
The core problem: dark mode that follows the sun
The behavior I wanted: dark mode turns on at sunset, off at sunrise, without any manual toggle, correctly scheduled for the user's location.
This required solving three sub-problems:
- Getting the user's location
- Computing sunset/sunrise times from coordinates
- Applying a CSS transformation to every page
Location: the Geolocation API
Firefox WebExtensions can call navigator.geolocation.getCurrentPosition() from a background script. I request permission once and cache the coordinates. I do not need precise location — the accuracy of city-level GPS is sufficient for sunset/sunrise within a few minutes.
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
browser.storage.local.set({ lat: latitude, lon: longitude });
},
(error) => {
// Fall back to manual time schedule if geolocation denied
}
);
The user can deny geolocation permission and fall back to a manual schedule (e.g., "dark mode from 8pm to 6am").
Sunset/sunrise calculation: the astronomy formula
I did not want an external API call for sunset times — that would add a network dependency and expose location data to a third party.
The sunset/sunrise calculation can be done in pure JavaScript using the NOAA solar algorithm. The formula takes latitude, longitude, and date, and returns sunrise and sunset in local time.
The key variables in the calculation are:
- Julian Day Number (days since January 1, 4713 BC)
- Hour angle at sunrise/sunset (derived from solar zenith angle)
- Equation of time (correction for the eccentricity of Earth's orbit)
- Solar declination (angle between sun and equator)
The full calculation is about 50 lines of math. The result is accurate to within a few minutes for most latitudes.
CSS injection: the content script
The dark mode transformation is applied by a content script that runs on every page. The approach I use is a CSS filter on the root element combined with an inverted filter on images to restore their natural colors:
html {
filter: invert(100%) hue-rotate(180deg);
}
img, video, iframe, canvas, picture {
filter: invert(100%) hue-rotate(180deg);
}
This is the "invert" approach — it inverts all colors (dark backgrounds become dark themes), then rotates the hue to restore natural color relationships. Images are double-inverted to appear normal.
The limitation: some sites with complex CSS or custom canvas rendering do not look right with this approach. For those, users can add them to an exclusion list.
Scheduling: alarms API
Firefox WebExtensions have an alarms API for scheduling callbacks. I set two alarms each day: one at sunrise and one at sunset. When an alarm fires, the background script toggles dark mode and reinjects the CSS.
browser.alarms.create('sunset', { when: sunsetTime });
browser.alarms.create('sunrise', { when: sunriseTime });
browser.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'sunset') enableDarkMode();
if (alarm.name === 'sunrise') disableDarkMode();
});
The result
NightOwl is about 300 lines of JavaScript: 50 lines for sunset math, 100 for the background scheduling logic, 100 for the content script, 50 for settings UI.
NightOwl is one of 11 Firefox extensions I built — see the full series.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.