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.
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 — react-spring-visualizer.com 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.
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.
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.
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.
Backlinks
- #1 — January 2024
- Choosing to remember with Anki
- CSS overscroll-behavior
- Interaction to Next Paint (INP)