fix(frontend): make dark mode toggle actually work

The dark mode button was toggling React state but never applying it to the
DOM. Add a useDarkMode hook that:

- Persists the preference in localStorage
- Defaults to the OS prefers-color-scheme on first visit
- Adds/removes the 'dark' class on <html> to trigger CSS variable overrides

Also add dark-mode color overrides for all Tailwind v4 theme variables in
index.css.

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
2026-06-11 21:50:13 +02:00
parent dd12631f07
commit 615e02e970
2 changed files with 60 additions and 26 deletions
+24 -25
View File
@@ -24,6 +24,7 @@ import BackupsPage from "./components/BackupsPage";
import { getOidcConfig, isOidcConfigured, setAccessToken } from "./auth";
import { fetchAppVersion } from "./api/client";
import { FRONTEND_VERSION_LABEL } from "./version";
import { usePersistentState } from "./hooks/usePersistentState";
import { Button } from "@/components/ui/button";
import {
Tooltip,
@@ -58,6 +59,24 @@ const queryClient = new QueryClient({
defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false } },
});
function useDarkMode() {
const [darkMode, setDarkMode] = usePersistentState<boolean>(
"dark-mode",
() => window.matchMedia("(prefers-color-scheme: dark)").matches,
);
useEffect(() => {
const root = document.documentElement;
if (darkMode) {
root.classList.add("dark");
} else {
root.classList.remove("dark");
}
}, [darkMode]);
return [darkMode, () => setDarkMode((prev) => !prev)] as const;
}
// Navigation items for sidebar
const navItems = [
{ path: "/", label: "Dashboard", icon: LayoutDashboard },
@@ -362,16 +381,7 @@ function SignInScreen({ onSignIn }: { onSignIn: () => void }) {
function AuthenticatedApp() {
const auth = useAuth();
const [darkMode, setDarkMode] = useState(() =>
window.matchMedia("(prefers-color-scheme: dark)").matches,
);
useEffect(() => {
const mql = window.matchMedia("(prefers-color-scheme: dark)");
const handler = (e: MediaQueryListEvent) => setDarkMode(e.matches);
mql.addEventListener("change", handler);
return () => mql.removeEventListener("change", handler);
}, []);
const [darkMode, toggleDarkMode] = useDarkMode();
useEffect(() => {
setAccessToken(auth.user?.access_token ?? null);
@@ -409,22 +419,13 @@ function AuthenticatedApp() {
darkMode={darkMode}
authLabel={authLabel}
onSignOut={() => void auth.signoutRedirect()}
onToggleDarkMode={() => setDarkMode(!darkMode)}
onToggleDarkMode={toggleDarkMode}
/>
);
}
function AppInner() {
const [prefersDarkMode, setPrefersDarkMode] = useState(() =>
window.matchMedia("(prefers-color-scheme: dark)").matches,
);
useEffect(() => {
const mql = window.matchMedia("(prefers-color-scheme: dark)");
const handler = (e: MediaQueryListEvent) => setPrefersDarkMode(e.matches);
mql.addEventListener("change", handler);
return () => mql.removeEventListener("change", handler);
}, []);
const [darkMode, toggleDarkMode] = useDarkMode();
return (
<QueryClientProvider client={queryClient}>
@@ -452,10 +453,8 @@ function AppInner() {
<Route
element={
<ShellLayout
darkMode={prefersDarkMode}
onToggleDarkMode={() =>
setPrefersDarkMode(!prefersDarkMode)
}
darkMode={darkMode}
onToggleDarkMode={toggleDarkMode}
/>
}
>