Visit homepage

Edison bulb night mode

  • Planted:
  • Last watered:

Up in the top right corner of the screen, there’s a little light bulb. It’s an SVG I made to resemble the Edison bulb I have in my apartment. You can click or drag and release to turn the lights on and off. All in all it’s around a couple hundred lines of code.

Night owls and early birds

Upon your first visit, an effect hook runs that taps into OS settings via the prefers-color-scheme CSS media query, defaulting to dark or light. You can play around with this by opening an incognito window and toggling your system theme setting or emulating CSS media features in Chrome DevTools.

When you click or drag the light bulb, your selection gets persisted in localStorage so that upon returning to my garden it’ll be either day or night, just like when you last visited.


useEffect(() => {
const storedThemePreference = JSON.parse(
if (storedThemePreference !== null) {
setLightModeState(storedThemePreference === "light" ? true : false);
const userPrefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
}, []);

It’s worth noting one shortcoming of this approach w/r/t SSR: localStorage and window.matchMedia are only available client-side, so the server-rendered HTML will always default to dark mode. That creates a flicker for early birds, so as a compromise I apply a CSS transition to color and background-color to create a fade-in effect. A few helpful commenters on Hacker News suggested either (1) moving the JS that checks localStorage into the document <head> to run before content loads, or (2) setting a cookie. I haven’t implemented either solution yet, but it’s on my todo list. I was also initially defaulting to light mode on the server, but another HN commenter pointed out that the light flicker is more jarring than vice versa.

Tactile animation

The drag ’n pull animation relies on react-spring, which aims to apply the fluidity of real world physics to web animations. Thanks to Josh Comeau’s introduction to spring physics and React Spring for the inspiration. Josh links to a few more resources in his blog post — is particularly neat.

React Spring exposes a useSpring hook that returns a style object for the element you want to animate. Its config accepts fields like tension and friction that mirror those real world properties.


const springProps = useSpring({
transform: clicked
? `translateY(${BOUNCE}px)`
: `translateY(${currentY - mouseDownY + OFFSET}px)`,
config: {
tension: 300,
friction: 20,

I’m using React state to manage mouse position and whether the light is being dragged or has been clicked. Event listeners for mousedown, mousemove, mouseup, and keyup capture user interaction to trigger theme updates and persistence. My initial implementation didn’t allow dragging on mobile because pull-to-refresh got in the way, but I’ve since added touch events (touchstart, touchmove, touchend) and CSS overscroll-behavior: none to enable touch dragging. Vercel’s automatic preview deployments were really handy for testing this quickly.

I could have achieved a similar drag ’n release effect with vanilla CSS transitions, but a personal website feels like the right place to experiment and indulge. @react-spring/web adds about 2.6MB / 51KB minified (20KB gzip). By comparison, react-dom and react come in at 4.9MB / 141KB minified (45KB gzip). I’d be curious to benchmark my page load speed with and without React Spring (or any package that adds material weight, really). Something to save for another Sunday morning...

I am using CSS transitions for a smooth fade between day and night. The SVG Edison bulb, which I sketched on paper then created in Figma, also transitions fill color between theme states. The mountain silhouettes in the footer also fade fill color as you turn off the lights (easiest to see on the homepage). And finally, my favicon is dynamic based on your OS-level theme preference, which is surprisingly easy to add with a media attribute.


media="(prefers-color-scheme: light)"
media="(prefers-color-scheme: dark)"

The click of the switch

My skeuomorphic light bulb wouldn’t be complete without the satisfying click of the switch. Toggle below to enable audio then turn the lights on/off!

I’m using an <audio> tag with a React ref to rig up sound.


/* runs on mount */
const audioRef = useRef<HTMLAudioElement>(null);
/* runs on onMouseUp, onKeyUp */
if (JSON.parse(localStorage.getItem(LocalStorageKey.AudioEnabled))) {
/* within returned JSX */
<audio ref={audioRef}>
<source src="./audio/light-switch.m4a" type="audio/mp4" />

For the actual audio clip, I spent five or 10 minutes searching for a nice free sound online before realizing I could just create a recording myself. So the sound you hear is a phone recording of me clicking the switch on my actual Edison bulb!

I disable audio by default and bury the option to enable audio within this Show ’n tell because I generally find sound on websites jarring and distracting. In this case, though, hearing the little sound makes me happy.

Getting accessibilty right

There are a few elements of the component design important for accessibility. For starters, using semantic HTML elements like button ensures the light bulb controls will be accessible via keyboard. You can reach the switch by tabbing and turn the lights on and off by pressing Enter or Space. I also added an aria-label attribute to the button and a visually hidden text label for screen reader users with the @radix-ui/react-visually-hidden utility. I considered whether exposing a theme to screen reader users would even be a useful feature, and I decided it’s best to keep the option open. Any feedback on this is welcome!

Front of the frontend

I fell into the classic developer trope of implementing dark mode before I’d published a single piece of writing, so documenting it felt like a good first step. I do love spending the odd Saturday working on front-of-the-frontend stuff like this, and personally I’m an early bird who much prefers light mode for most everything outside my code editor. Maintaining two themes adds some overhead, but I’m happy to do it for the night owls.