Dark Map, Flat Roads: Google Maps Dark Mode via Runtime Overrides
Fiord made the map dark but flat. I mutate the style JSON at runtime: a road brightness ladder, bright haloed labels, and a guard against fast-toggle flashes.
TL;DR
The author built a dark mode for Fiord by fetching its JSON and overriding paint across 28 layers with brightness steps, since CSS filters broke markers and the built-in dark style lacked contrast. The map renders the base style instantly then upgrades when the mutated style resolves, with guards against stale toggles. This treats style JSON as mutable data, not a fixed product.
[unverified-own-experience] The fiord style landed the day before, and I opened the map again right away. Dark, finally. Then I tried to find the main road among the residential ones and nearly failed. Every road sat on the same plane of gray. Not the night-mode feel of Google Maps mobile where the highway pops, but more like a map covered with gray tracing paper.
My first guess: slap a CSS invert(1) filter on when dark mode is on. One line, done. Second guess: find a better dark style. Both dead ends. A CSS filter flips the markers and labels along with the roads, and OpenFreeMap's own dark style I had already measured the day before at a 1.03:1 contrast, rendered but effectively invisible. The OpenFreeMap README [8] is honest about it: Dark and Fiord are inherited from OpenMapTiles styles whose upstream is abandoned, and only Liberty gets active care. Waiting for a better dark style to appear was not a plan.
setStyle takes an object, not just a URL
I had always assumed map.setStyle() accepted a style URL string, period. Want your own look? Fork the JSON, host it yourself, point the URL at your repo. Heavy machinery for something that only wants different colors.
The API docs [3] say the style parameter is string | StyleSpecification: a full JSON object conforming to the style spec, or a URL to one. The official door was open all along: fetch the fiord JSON, override its paint per layer, hand the object to setStyle. Google does the same thing for night mode, an array of JSON rules that recolor map elements [4]. The only difference is Google has stylers and I have layers[].paint.
A brightness ladder, not just dark
I fetch the fiord URL once per session through a cached Promise, then override paint for roughly 28 layer ids I verified one by one against the live JSON. The trick is not "make it black" but a brightness ladder: darkest background #1b1f27, paths #2a303c, minor roads #333a48, major roads #4a5468, up to motorways brightest at #5a6678. The eye keeps reading the road hierarchy on top of near-black navy.
The labels are what makes it feel like Google Maps: city names #e8eaed with a dark 1px text halo in #1b1f27, villages and suburbs dimmer at #c7cbd1, street names dimmest at #9aa0a6. That dark halo behind bright text keeps every label readable over any road.
Paint immediately, upgrade later, never flash
Waiting for the fetch before creating the map would leave users staring at an empty div. So my components paint the raw fiord URL first so the map appears instantly, and upgrade to the mutated style once the fetch resolves. There is a brief window of plain fiord before the night version lands, but that is imperceptible next to a blank div.
Second trap: a fast dark-light toggle. An old Promise can resolve after the user is already back in light mode, and its setStyle paints a dark map in light mode. The guard is simple: before calling setStyle, check mapRef.current === map && styleRef.current === "dark". If the world moved on, the stale resolution gets dropped silently. If the fetch fails, the map stays plain fiord instead of breaking. If OpenFreeMap ever renames a layer, an override that finds no matching id just no-ops. The worst failure mode of the whole chain is the map falling back to its default look, which is exactly what I want.
What changed is how I see third-party styles: the style JSON is raw material, not a finished product. The spec defines a style as a document describing what to draw and how to color it, and because it is just data, it can be mutated like data [1]. There is also a first-class option called transformStyle that modifies a style after it is fetched but before it is committed to the map, and that is the more idiomatic choice if you start from scratch [7]. For my case, one dark style shared by three components, fetch once, mutate once, share everywhere stayed the simplest shape.