Supporting Dark Mode in the Blog
I was thinking that the original theme of the blog looks nice, but is also kinda bright. So why not be modern and support a dark mode theme for users with such an activated setting?
The rough Guideline
I was looking for a tutorial how to support a dark mode Jekyll theme and luckily this blog explains it very well. I will roughly sketch the steps and move on with some more detailed explanations of the problems I faced.
The basic idea - as described in the blog above - is to convert all your colors in the CSS to use CSS-variables. Those variables are widely supported, so it should be safe to use them.
Once we use such variables all over our place we can use a @media (prefers-color-scheme: dark)
query to change the values of these variables if the user has a system with dark mode settings enabled. That’s it! In code this looks like
// your entry scss file
:root {
--text-color-main: #333333;
}
@media (prefers-color-scheme: dark) {
:root {
--text-color-main: #cccccc;
}
}
html, body {
color: var(--text-color-main);
}
This blog is based on Jekyll Now on Github, which is using a default theme with Sass and its variables. Technically I should have been able to work with these Sass variables, but I noticed that there were some parts in the code (syntaxx highlighting) that had hard-coded color values. Not so good I thought. Additionally most Frameworks I work with in my free time and during work are relying on CSS variables rather than Sass variables for their theming. So I refactored the code base slightly to CSS variables.
Being Lazy
Once I came to the Syntax Hightlighting I noticed that I don’t want to manually adjust all of these 60+ slots and replace the color values with variable names… Lazy as your usual developer I thought how to solve this problem and came up with a small script, that processes the original file row by row, produces a css variable for light mode, one for dark mode (simply inverted color),
Disclaimer: After I finished my work I noticed that I didn’t had to do this, because the default colors work kind of fine with the dark theme. Also the inverted colors are not perfect. Anyhow! 😁
The original file looked like
.highlight .c { color: #586E75 } /* Comment */
.highlight .err { color: #93A1A1 } /* Error */
.highlight .g { color: #93A1A1 } /* Generic */
// and 50+ more lines like these
First step was to slightly preprocess the file by removing all .highlight
classes. Not necessary, but makes it a bit easier for the humand mind to process.
Afterwards I wrote a simple Node.js script (file ending .mjs
), that reads the file line by line, extract the color value via Regex /#[A-F0-9]{6}/
and the comment value - also via Regex /\/\* [\w.]* \*\//
. Both were used to generate a variable name with a color value. Afterwards the original color value was replaced by the usage of the CSS variable.
The result looks like this (truncated):
:root {
--syntax-hightlight-color-comment: #586e75; /* Comment */
--syntax-hightlight-color-error: #93a1a1; /* Error */
--syntax-hightlight-color-generic: #93a1a1; /* Generic */
}
.hightlight {
.c { color: var(--syntax-hightlight-color-comment); } /* Comment */
.err { color: var(--syntax-hightlight-color-error); } /* Error */
.g { color: var(--syntax-hightlight-color-generic); } /* Generic */
}
Additionally I went ahead an inverted the original color value. This can be done by subtracting the RGB values of a color from 255 (each of them). Then I put those inverted colors into the dark mode @media
query in my output file.
As I said above I don’t use those values for now, because they don’t match my expectations - it seems that simply inverted so many colors don’t work out well. For the basic set tho (i.e. blog texts and background) inverting works fine. Nevertheless I need to adjust that at some point in time and for that I’m grateful that I have the variables.
The following script must be saved as an .mjs
file and you need a recent Node.js version (like 14 or 15) to execute it. Otherwise refactor the import
statements to require
and run it as usual.
// convert.mjs
import readline from 'readline';
import fs from 'fs';
const readInterface = readline.createInterface({
input: fs.createReadStream('./input.txt'),
output: null,
console: false
});
let cssVarLight = '';
let cssVarDark = '';
let cssRules = '';
readInterface.on('line', line => {
// extract color
const colorRegex = /#[A-F0-9]{6}/;
const lightColor = line.match(colorRegex)[0]
const lightRgb = hexToRgb(lightColor);
// create css variable name
const commentRegex = /\/\* [\w.]* \*\//;
const commentText = line.match(commentRegex)[0];
const cssVarName = '--syntax-hightlight-color-' + commentText
.replace('/* ', '').replace(' */', '')
.toLowerCase()
.replace(/\./g, '-');
// print css rule
cssRules += '\n' + line.replace(lightColor, `var(${cssVarName})`);
// print css variable
cssVarLight += `\n${cssVarName}: ${lightColor}; ${commentText}`;
// dark mode
const invertedRgb = invertRgb(lightRgb);
const invertedColor = rgbToHex(invertedRgb);
cssVarDark += `\n${cssVarName}: ${invertedColor}; ${commentText}`;
});
readInterface.on('close', () => {
fs.writeFileSync('css-rules.css', cssRules);
fs.writeFileSync('css-vars-light.css', cssVarLight);
fs.writeFileSync('css-vars-dark.css', cssVarDark);
});
function invertRgb(rgb) {
return {
r: 255 - rgb.r,
g: 255 - rgb.g,
b: 255 - rgb.b,
}
}
// source: https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb#answer-5624139
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function componentToHex(c) {
var hex = c.toString(16);
return hex.length == 1 ? "0" + hex : hex;
}
function rgbToHex(rgb) {
return "#" + componentToHex(rgb.r) + componentToHex(rgb.g) + componentToHex(rgb.b);
}