Dark theme in a day

Using a bunch of modern CSS to create a night mode for an app

I recently spent some time with a startup called Kite to work on the typography of their tools. Among their group of smart utilities helping programmers code, the centerpiece is a native/web app named Kite Copilot that sits by your editor to lend a helping hand. It looks like this:

CSS variables

I knew immediately that this project could not happen without CSS variables, and I was excited to try them for the first time in native CSS (I’ve only known them before courtesy of LESS). I started by turning all the existing hardcoded colors into variables, and then reducing the number of variables to the absolute minimum. It looked something like this:

Before.main {
background-color: #eef4f7;
color: #0a244d;
}
.docs {
color: #0c2959;
}
Afterhtml {
--background: #eef4f7;
--text-color-normal: #0a244d;
}
.main {
background: var(--background);
color: var(--text-color-normal);
}
.docs {
color: var(--text-color-normal);
}
Before.home {
color: #0a244d;
}
.home__version {
opacity: 0.7;
}
Afterhtml {
--text-color-normal: #0a244d;
--text-color-light: #8cabd9;
}
.home {
color: var(--text-color);
}
.home__version {
color: var(--text-color-light);
}

Expressing colours

Then, I took variables one tiny step further. I didn’t want to assume that the colours would always come from the Design world and the job of Engineering would only be to plug them in. There was a practical reason, too: playing with colours live by editing the file seemed like the best way to optimize the palette.

Beforehtml {
--text-color-normal: #0a244d;
--text-color-light: #8cabd9;
}
Afterhtml[data-theme='dark'] {
--text-color-normal: hsl(210, 10%, 62%);
--text-color-light: hsl(210, 15%, 35%);
--text-color-richer: hsl(210, 50%, 72%);
--text-color-highlight: hsl(25, 70%, 45%);
}
html[data-theme='dark'] {
--hue: 210; /* Blue */
--accent-hue: 25; /* Orange */
--text-color-normal: hsl(var(--hue), 10%, 62%);
--text-color-highlight: hsl(var(--accent-hue), 70%, 45%);
}
html[data-theme='dark'] {
--hue: 210; /* Blue */
--accent-hue: 25; /* Orange */
--text-color-normal: hsl(var(--hue), 10%, 62%);
--text-color-light: hsl(var(--hue), 15%, 35%);
--text-color-richer: hsl(var(--hue), 50%, 72%);
--text-color-highlight: hsl(var(--accent-hue), 70%, 45%);
--link-color: hsl(var(--hue), 90%, 70%);
--accent-color: hsl(var(--accent-hue), 100%, 70%);
--error-color: rgb(240, 50, 50);
--button-background: hsl(var(--hue), 63%, 43%);
--button-text-color: black;
--background: hsl(var(--hue), 20%, 12%);
}

Switching between themes

The theme was applied to a data attribute on top of <html>. A class would be okay, too, but in my head classes feel like checkboxes, whereas data attributes act more like radio buttons. It felt like a moot distinction at this very point — we only had two themes — but I wanted to think a bit ahead.

Definition in CSShtml {
--hue: 210; /* Blue */
--text-color-normal: hsl(var(--hue), 77%, 17%);
...
}
html[data-theme='dark'] {
--text-color-normal: hsl(var(--hue), 10%, 62%);
...
}
Invocation in JavaScriptdocument.documentElement.setAttribute('data-theme', 'dark')
document.documentElement.setAttribute('data-theme', 'light')
html.color-theme-in-transition,
html.color-theme-in-transition *,
html.color-theme-in-transition *:before,
html.color-theme-in-transition *:after {
transition: all 750ms !important;
transition-delay: 0 !important;
}
document.documentElement.classList.add('color-theme-in-transition')
document.documentElement.setAttribute('data-theme', theme)
window.setTimeout(function() {
document.documentElement.classList.remove('color-theme-in-transition')
}, 1000)

Theming vector icons

The colour variables took great care of text. But then the dreaded moment came when it came to taking care of the visual elements. A bunch of iconography in Kite’s Copilot existed as vector SVGs as backgrounds:

.sidebar__icon__settings {
width: 2rem;
height: 2rem;
background-image: url(./icon-settings.svg);
}
mask-image: url(./icon-refresh-white.svg);
mask-repeat: no-repeat;
mask-position: center;
background: blue;
Beforebackground-size: contain;
background-repeat: no-repeat;
background-position: center;
background-size: 1rem;
background-image: url(./icon-back.svg);
Aftermask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
mask-size: 1rem;
mask-image: url(./icon-back.svg);
background-color: var(--link-color);

Theming images

Kite also has regular bitmapped images interspersed through some of the developer documentation. They looked something like this:

html[data-theme='dark'] img {
filter: invert(100%);
}
html[data-theme='dark'] img {
filter: invert(100%) hue-rotate(180deg);
}
html[data-theme='dark'] img {
filter: invert(100%) hue-rotate(180deg);
mix-blend-mode: screen;
}
img {
mix-blend-mode: multiply;
}
html[data-theme='dark'] img {
filter: invert(100%) hue-rotate(180deg);
mix-blend-mode: screen;
}

Theming the scrollbar

Well, almost complete. If images and icons kept their bright liveries and had to darkened, we faced the opposite issue with Mac’s autohiding scrollbar — it was black (and basically invisible) even in the dark mode.

.docs-page__content {
background: var(--background);
overflow-y: auto;
}

A high contrast theme

I also wanted to extend this project in another direction. I added one more theme — a high contrast one:

html[data-theme='high-contrast'] {
--text-color-normal: white;
--text-color-light: white;
--text-color-richer: white;
--text-color-highlight: white;
--link-color: white;
--bright-color: white;
--error-color: white;
--button-background: white;
--button-text-color: black;
--background: black;
--popup-background: black;
}
html {
--popup-shadow: 0 5px 16px rgba(0, 0, 0, .25);
}
html[data-theme='dark'] {
--popup-shadow: 0 5px 32px black;
}
html[data-theme='high-contrast'] {
--popup-shadow: 0 0 5px white, 0 0 5px white;
}

Designer/typographer · Writing a book on the history of keyboards: https://aresluna.org/shift-happens