I added dark/light mode (themes) to existing Deno websites (fresh.deno.dev/docs and docs.deno.com). Here is what I learned and how you can eventually implement my solution on Deno Fresh apps.
Recently I had the privilege of helping to add light/dark mode toggling to Deno's documentation site (built with Lume) and the Fresh framework documentation (built with Fresh). The goal was to implement a seamless, user-friendly theme switcher while keeping the solution as simple and maintainable as possible.
You can see the long history for each of these in the pull requests linked at the bottom of this post.
@tailwindcss/forms
Since both sites already used Tailwind CSS, integrating dark mode was straightforward. The challenges were ensuring that:
To achieve this, I set the data-theme
(or class
) attribute on the html
tag
of each page to either dark
or light
depending on a few factors (in order,
and depending on if the setting exists):
<ThemeToggle />
then localStorage.theme is set so use that).prefers-color-scheme
(so we know if the
browser or OS has a preference set).The Deno Docs website uses Lume and the Fresh Docs website uses (not surprisingly) Fresh. The Lume implementation was and is a bit less interesting to me considering that my personal website (and a project I recently released, AgapeVerse) use Fresh. For the Fresh Docs website I ended up creating an Island that pretty cleanly adds almost everything needed.
Here is what I did:
/island/ThemeToggle.tsx
with the following logic:Detect the user's preferred theme: On initial page load, check
localStorage
for a saved preference. If none exists, fallback to
window.matchMedia('(prefers-color-scheme: dark)')
.
const [theme, setTheme] = useState(() => {
if (!IS_BROWSER) return "light";
return document.documentElement.dataset.theme ?? "light";
});
const ThemeToggle = () => {
setTheme((prev) => {
const theme = prev === "light" ? "dark" : "light";
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem("theme", theme);
return theme;
});
};
<head>
to the _app.tsx
fileThe following requirements could be accomplished just from the Island component
however: To avoid FOUC (Flash of Unstyled Content) it's necessary to add the
following to the page in a <script>
tag to the <head>
element.
const isDarkMode = localStorage.theme === "dark" ||
(!("theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches);
document.documentElement.dataset.theme = isDarkMode ? "dark" : "light";
There are several ways this could be done, and while this isn't what was used on the Fresh Docs page this is how I feel it makes the most sense to do it:
# _app.tsx
import { PageProps } from "fresh";
import { themeToggleHeadScript } from "$/island/ThemeToggle.tsx";
...
<head>
...
<script src={`data: text/javascript, ${themeToggleHeadScript}`}></script>
</head>
...
styles.css
or similar
create a set of CSS vars (as defaults and for the dark version of each color,
example below).@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--foreground: 34, 73%, 10%;
--background: 264, 100%, 90%;
--primary: 265, 95%, 57%;
--secondary: 34, 96%, 52%;
--accent: 135, 73%, 49%;
--link-blue: 220, 80%, 50%;
}
.html[data-theme="dark"]:root {
--foreground: 34, 73%, 90%;
--background: 264, 100%, 10%;
--primary: 265, 95%, 43%;
--secondary: 34, 96%, 48%;
--accent: 135, 73%, 51%;
--link-blue: 220, 80%, 50%;
}
}
Here's a snippet of the core logic:
function setTheme(theme: "light" | "dark") {
if (theme === "dark") {
document.documentElement.classList.add("dark");
localStorage.setItem("theme", "dark");
} else {
document.documentElement.classList.remove("dark");
localStorage.setItem("theme", "light");
}
}
function initTheme() {
const storedTheme = localStorage.getItem("theme");
if (storedTheme) {
setTheme(storedTheme as "light" | "dark");
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
setTheme("dark");
}
}
document.addEventListener("DOMContentLoaded", initTheme);
After implementing dark mode for these two sites, I realized that many Fresh-based projects might benefit from a simple drop-in solution for theme toggling. To address this, I’m now working on a Deno Fresh plugin that will make it easy for any Fresh website to support dark mode with minimal setup.
dark:
class strategy.The plugin is still in early development, but once complete, it will provide:
<ThemeToggle />
for a dark/light/system
mode toggle button.I’ll be publishing the plugin on JSR soon, along with installation and usage instructions. Stay tuned!
If you're building a Fresh app and want easy dark mode support, I'd love to hear your thoughts. Let me know what features you'd find useful!