How to Add Dark Mode to Your Jekyll Site
Add a dark mode toggle to any Jekyll site — using CSS variables, a JavaScript toggle, and localStorage to remember the user's preference.
Dark mode is no longer optional — it’s expected. This guide shows you how to add a proper dark mode toggle to any Jekyll site, with the user’s preference saved across visits.
The Approach
We will use three techniques together:
- CSS custom properties (variables) — define colours once, swap them all at once
- JavaScript toggle — switch modes on button click
- localStorage — remember the user’s choice on return visits
prefers-color-schememedia query — respect the OS-level dark mode preference on first visit
Step 1: Restructure Your CSS with Variables
Instead of hard-coding colours, define them as CSS custom properties:
/* _sass/_variables.scss */
:root {
--bg-color: #ffffff;
--text-color: #1a1a1a;
--text-muted: #6b7280;
--border-color: #e5e7eb;
--card-bg: #f9fafb;
--link-color: #2563eb;
--heading-color: #111827;
--code-bg: #f3f4f6;
}
[data-theme="dark"] {
--bg-color: #0f172a;
--text-color: #e2e8f0;
--text-muted: #94a3b8;
--border-color: #334155;
--card-bg: #1e293b;
--link-color: #60a5fa;
--heading-color: #f1f5f9;
--code-bg: #1e293b;
}
Now use these variables throughout your stylesheets:
body {
background-color: var(--bg-color);
color: var(--text-color);
}
a {
color: var(--link-color);
}
h1, h2, h3 {
color: var(--heading-color);
}
When you switch from [data-theme="light"] to [data-theme="dark"] on the <html> element, every colour updates instantly with no additional CSS.
Step 2: Respect the System Preference
Users who have set their OS to dark mode expect your site to default to dark. Use the prefers-color-scheme media query:
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg-color: #0f172a;
--text-color: #e2e8f0;
--text-muted: #94a3b8;
--border-color: #334155;
--card-bg: #1e293b;
--link-color: #60a5fa;
--heading-color: #f1f5f9;
--code-bg: #1e293b;
}
}
The :not([data-theme="light"]) selector means: use dark colours unless the user has explicitly chosen light mode via the toggle.
Step 3: Add the Toggle Button
In your header HTML (e.g. _includes/header.html):
<button id="theme-toggle" aria-label="Toggle dark mode" class="theme-toggle">
<span class="theme-toggle__sun">☀️</span>
<span class="theme-toggle__moon">🌙</span>
</button>
Style it to show the sun in dark mode and the moon in light mode:
.theme-toggle {
background: none;
border: 1px solid var(--border-color);
border-radius: 50%;
width: 36px;
height: 36px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
transition: border-color 0.2s;
&:hover {
border-color: var(--link-color);
}
}
/* Show sun in dark mode (to switch to light), moon in light mode (to switch to dark) */
[data-theme="dark"] .theme-toggle__moon { display: none; }
[data-theme="dark"] .theme-toggle__sun { display: inline; }
.theme-toggle__sun { display: none; }
.theme-toggle__moon { display: inline; }
Step 4: Write the JavaScript
Create assets/js/theme-toggle.js:
(function() {
const STORAGE_KEY = 'theme';
const DARK = 'dark';
const LIGHT = 'light';
function getPreferredTheme() {
// Check localStorage first
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) return stored;
// Fall back to OS preference
return window.matchMedia('(prefers-color-scheme: dark)').matches ? DARK : LIGHT;
}
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
}
// Apply theme immediately (before page renders) to prevent flash
applyTheme(getPreferredTheme());
// Set up toggle button after DOM loads
document.addEventListener('DOMContentLoaded', function() {
const button = document.getElementById('theme-toggle');
if (!button) return;
button.addEventListener('click', function() {
const current = document.documentElement.getAttribute('data-theme');
const next = current === DARK ? LIGHT : DARK;
applyTheme(next);
localStorage.setItem(STORAGE_KEY, next);
});
});
})();
Important: Load this script in <head> to prevent a flash of the wrong theme:
<!-- _includes/head.html -->
<script src="{{ '/assets/js/theme-toggle.js' | relative_url }}"></script>
Loading it in <head> (not deferred) means the theme is applied before the page renders — no white flash before dark mode kicks in.
Step 5: Handle Images in Dark Mode
Light-background images can look jarring in dark mode. Options:
Option A: Reduce opacity for images in dark mode:
[data-theme="dark"] img:not([src*=".svg"]) {
opacity: 0.9;
}
Option B: Use the <picture> element to serve different images per mode:
<picture>
<source srcset="/assets/images/logo-dark.png" media="(prefers-color-scheme: dark)">
<img src="/assets/images/logo-light.png" alt="Logo">
</picture>
Option C: Use SVGs with currentColor for icons and logos — they automatically adapt to the text colour.
Step 6: Dark Mode for Syntax Highlighting
Jekyll uses Rouge or Pygments for syntax highlighting. You need a dark colour scheme.
Add to your _config.yml:
highlighter: rouge
Then download a dark theme for Rouge. The Base16 Ocean Dark theme is a popular choice. Add it to _sass/:
/* _sass/_syntax-dark.scss */
[data-theme="dark"] .highlight {
background: #2b303b;
color: #c0c5ce;
}
[data-theme="dark"] .highlight .k { color: #b48ead; } /* keyword */
[data-theme="dark"] .highlight .s { color: #a3be8c; } /* string */
[data-theme="dark"] .highlight .c { color: #65737e; } /* comment */
[data-theme="dark"] .highlight .n { color: #c0c5ce; } /* name */
[data-theme="dark"] .highlight .o { color: #8fa1b3; } /* operator */
Testing Your Dark Mode
- Open your site, toggle dark mode — all colours should switch cleanly
- Refresh the page — dark mode should persist (localStorage is working)
- Open DevTools → Application → Local Storage — you should see
theme: dark - Set your OS to dark mode and visit the site for the first time in an incognito window — it should default to dark
Dark Mode in Jekyll Themes
Many modern Jekyll themes already include dark mode. If you would rather use a theme that has it built in, check out Chirpy (seamless dark/light toggle) or browse all themes with dark mode support on JekyllHub.