htsx

Dark Mode

Easy way to set up dark mode in Hono projects.

1. Add dual theme variants

The below is Neutral colorway. You can customize it anytime.

src/style.css
@import "tailwindcss" source("../src");
 
@custom-variant dark (&:is(.dark *));
 
@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-card: var(--card);
  --color-card-foreground: var(--card-foreground);
  --color-popover: var(--popover);
  --color-popover-foreground: var(--popover-foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-secondary: var(--secondary);
  --color-secondary-foreground: var(--secondary-foreground);
  --color-muted: var(--muted);
  --color-muted-foreground: var(--muted-foreground);
  --color-accent: var(--accent);
  --color-accent-foreground: var(--accent-foreground);
  --color-destructive: var(--destructive);
  --color-border: var(--border);
  --color-input: var(--input);
  --color-ring: var(--ring);
}
 
:root {
  color-scheme: light;
  --background: oklch(100% 0 0);
  --foreground: oklch(14.5% 0 0);
  --card: oklch(98.5% 0 0);
  --card-foreground: oklch(20.5% 0 0);
  --popover: oklch(100% 0 0);
  --popover-foreground: oklch(14.5% 0 0);
  --primary: oklch(70.5% 0.213 47.604);
  --primary-foreground: oklch(98.5% 0 0);
  --secondary: oklch(76.9% 0.188 70.08);
  --secondary-foreground: oklch(98.5% 0 0);
  --muted: oklch(98.5% 0 0);
  --muted-foreground: oklch(43.9% 0 0);
  --accent: oklch(97% 0 0);
  --accent-foreground: oklch(14.5% 0 0);
  --destructive: oklch(63.7% 0.237 25.331);
  --border: oklch(92.2% 0 0);
  --input: oklch(92.2% 0 0);
  --ring: oklch(92.2% 0 0);
}
 
.dark {
  color-scheme: dark;
  --background: oklch(14.5% 0 0);
  --foreground: oklch(98.5% 0 0);
  --card: oklch(20.5% 0 0);
  --card-foreground: oklch(97% 0 0);
  --popover: oklch(14.5% 0 0);
  --popover-foreground: oklch(98.5% 0 0);
  --primary: oklch(70.5% 0.213 47.604);
  --primary-foreground: oklch(98.5% 0 0);
  --secondary: oklch(76.9% 0.188 70.08);
  --secondary-foreground: oklch(98.5% 0 0);
  --muted: oklch(20.5% 0 0);
  --muted-foreground: oklch(70.8% 0 0);
  --accent: oklch(20.5% 0 0);
  --accent-foreground: oklch(97% 0 0);
  --destructive: oklch(63.7% 0.237 25.331);
  --border: oklch(37.1% 0 0);
  --input: oklch(37.1% 0 0);
  --ring: oklch(37.1% 0 0);
}

Choose your dark mode style

A. Dual mode (Dark / Light)

1. Add scripts

The first script is runs before first paint to avoid FOUC. The second module runs after the DOM is ready.

renderer.tsx
import { html } from "hono/html";
 
<head>
  {html`
    <script>
      document.documentElement.classList.toggle("dark", localStorage.theme !== "light");
    </script>
    <script type="module">
      document.getElementById("theme-toggle").addEventListener("click", () => {
        localStorage.theme = document.documentElement.classList.toggle("dark") ? "dark" : "light";
      });
    </script>
  `}
</head>;

2. Add switcher (button)

Both icons are always in the DOM. No layout shift occurs.

header.tsx
<button id="theme-toggle" type="button" aria-label="Toggle theme">
  <span class="hidden dark:block">☀️</span>
  <span class="dark:hidden">🌙</span>
</button>

B. Tri mode (System / Dark / Light)

1. Add scripts

The first script is runs before first paint to avoid FOUC. The second module runs after the DOM is ready.

renderer.tsx
import { html } from "hono/html";
 
<head>
  {html`
    <script>
      function applyDark() {
        document.documentElement.classList.toggle(
          "dark",
          localStorage.theme === "dark" ||
            (!("theme" in localStorage) && matchMedia("(prefers-color-scheme: dark)").matches),
        );
      }
      applyDark();
    </script>
    <script type="module">
      const s = document.getElementById("theme-toggle");
      s.value = localStorage.theme ?? "system";
      s.addEventListener("change", () => {
        s.value === "system" ? localStorage.removeItem("theme") : (localStorage.theme = s.value);
        applyDark();
      });
    </script>
  `}
</head>;

2. Add switcher (select)

header.tsx
<select id="theme-toggle" aria-label="Theme">
  <option value="system">System</option>
  <option value="light">Light</option>
  <option value="dark">Dark</option>
</select>