Themer and how you can handle dark mode a lot more gracefully

A few days back I was basically redesigning the long lost
todo app from my repositories and I ended up liking my
selected color scheme and the dark variant of it. This lead to a simple dark and
light toggle that I wrote in about 20 lines of JS, by simply changing a key in
the local storage and handling that change and edge case accordingly.

10 mins after this, I realised the the
commitlog-web could take advantage of the
new color scheme and the web version of it is written in golang and html
templates so I needed something vanilla so I just ended up using the above code
from the todo implementation. At this point, it's all good, but then a small
issue. It'd take the stored theme instead of the system preferred theme only and
for someone who's theme changes automatically over the course of the day , this
was a problem.

Now most people would be fine with just the prefers-color-scheme media query
but now I don't assume what scheme the user would want to use for my particular
app so I want him to be able to choose between system, light, dark and now this
is where themer got created.

It's like 200 lines and you can probably understand by reading the source code ,
but I'll get through the algorithm just in case.

Source Code

Also, you can just install themer and use it if
you'd find that easier but here goes.

Requirements

  1. Ability to switch between system,light,dark.
  2. As a developer, the developer experience to just add in one button , point
    the library to it and have it work seamlessly.
  3. As a developer, the ability to customize the toggles when needed so a
    function export that can handle the same context.
  4. Permanent storage of the selected theme.

The Plan

  1. Since there's a need for context, we are going to use a Prototype Function
    declaration for this library (more on that in a few mins).
  2. Ability to customize the button, so the button won't be created dynamically
    but picked from the config provided to the library, though I wanted a quick
    setup so the library will handle the icons inside the button, just not the
    button creation and styling.
  3. Write a function that can be exposed to the instance so that if needed, the
    person can create custom toggles programmatically.

Code Flow

  1. We define a prototype function first. A prototype function is basically the
    vanilla js way of making/writing classes , give you the ability to add
    pre-defined methods to an instance created via the function as a constructor,
    an example of this would be Date

So, first piece of code.

function Themer() {}
  1. We need it to accept a config so that we can select if we want to handle the
    toggle ourselves or we want the user to handle it for us. Also, we will see
    if there's an existing theme value the user has or not.
function Themer(config) {
  let element = config.trigger;
  if (element) {
    // Check if the trigger was passed a class string or an id string and convert it to a proper html node ref
    if (typeof config.trigger === "string")
      element = document.querySelector(config.trigger);
  }

  // existing state for the theme , fallback to system if nothing is found
  const defaultState = localStorage.getItem("theme") || "system";
}
  1. Now, for the actual toggle, all we do is set the body tag to have an
    attribute called data-dark-mode and if this is present, your css can
    over-ride the default light mode variables or you can write custom css with
    this as a selector.
body[data-dark-mode] button {
  background: white;
  color: #121212;
}

though, just resetting the variables would be easier, you can find an
example here

  1. All that's left is to find out which theme we are on and which the next one
    is supposed to be and this is done on the click on the trigger, also,
    remember we have to expose the function so we have to isolate that logic and
    also we need to make sure the same functions are also executed when the
    system preference changes if the set theme is on system

No use posting the snippet cause that's the whole
index.js which
you can read.

Hope you liked the post,

Adios!