Implement robust dark mode with system preference detection and persistence in Google Antigravity apps.
# Dark Mode Implementation for Google Antigravity
Implement a complete dark mode solution with system preference detection, user preference persistence, and smooth transitions for your Google Antigravity applications.
## Theme Context Provider
```typescript
// contexts/ThemeContext.tsx
"use client";
import { createContext, useContext, useEffect, useState, useCallback } from "react";
type Theme = "light" | "dark" | "system";
type ResolvedTheme = "light" | "dark";
interface ThemeContextType {
theme: Theme;
resolvedTheme: ResolvedTheme;
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
const STORAGE_KEY = "theme-preference";
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setThemeState] = useState<Theme>("system");
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>("light");
const [mounted, setMounted] = useState(false);
const getSystemTheme = useCallback((): ResolvedTheme => {
if (typeof window === "undefined") return "light";
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}, []);
const applyTheme = useCallback((newTheme: Theme) => {
const resolved = newTheme === "system" ? getSystemTheme() : newTheme;
setResolvedTheme(resolved);
document.documentElement.classList.remove("light", "dark");
document.documentElement.classList.add(resolved);
document.documentElement.style.colorScheme = resolved;
}, [getSystemTheme]);
const setTheme = useCallback((newTheme: Theme) => {
setThemeState(newTheme);
localStorage.setItem(STORAGE_KEY, newTheme);
applyTheme(newTheme);
}, [applyTheme]);
const toggleTheme = useCallback(() => {
const newTheme = resolvedTheme === "light" ? "dark" : "light";
setTheme(newTheme);
}, [resolvedTheme, setTheme]);
useEffect(() => {
const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;
const initialTheme = stored || "system";
setThemeState(initialTheme);
applyTheme(initialTheme);
setMounted(true);
}, [applyTheme]);
useEffect(() => {
if (theme !== "system") return;
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = () => applyTheme("system");
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}, [theme, applyTheme]);
if (!mounted) {
return <>{children}</>;
}
return (
<ThemeContext.Provider value={{ theme, resolvedTheme, setTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error("useTheme must be used within ThemeProvider");
return context;
}
```
## Theme Toggle Component
```typescript
// components/ThemeToggle.tsx
"use client";
import { useTheme } from "@/contexts/ThemeContext";
import { useState, useRef, useEffect } from "react";
export function ThemeToggle() {
const { theme, resolvedTheme, setTheme } = useTheme();
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
setOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const themes: { value: "light" | "dark" | "system"; label: string; icon: string }[] = [
{ value: "light", label: "Light", icon: "āļø" },
{ value: "dark", label: "Dark", icon: "š" },
{ value: "system", label: "System", icon: "š»" },
];
return (
<div ref={ref} className="theme-toggle-container">
<button onClick={() => setOpen(!open)} className="theme-toggle-button" aria-label="Toggle theme">
{resolvedTheme === "dark" ? "š" : "āļø"}
</button>
{open && (
<div className="theme-dropdown">
{themes.map(({ value, label, icon }) => (
<button
key={value}
onClick={() => { setTheme(value); setOpen(false); }}
className={`theme-option ${theme === value ? "active" : ""}`}
>
<span>{icon}</span>
<span>{label}</span>
{theme === value && <span className="check">ā</span>}
</button>
))}
</div>
)}
</div>
);
}
```
## CSS Custom Properties
```css
/* globals.css */
:root {
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--bg-tertiary: #e5e5e5;
--text-primary: #1a1a1a;
--text-secondary: #4a4a4a;
--text-muted: #8a8a8a;
--border-color: #e0e0e0;
--accent-color: #3b82f6;
--accent-hover: #2563eb;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--transition: 200ms ease;
}
.dark {
--bg-primary: #0a0a0a;
--bg-secondary: #1a1a1a;
--bg-tertiary: #2a2a2a;
--text-primary: #fafafa;
--text-secondary: #a0a0a0;
--text-muted: #666666;
--border-color: #333333;
--accent-color: #60a5fa;
--accent-hover: #93c5fd;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
html {
transition: background-color var(--transition), color var(--transition);
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.card {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
box-shadow: var(--shadow);
}
.button-primary {
background-color: var(--accent-color);
color: white;
}
.button-primary:hover {
background-color: var(--accent-hover);
}
```
## Flash Prevention Script
```typescript
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
const stored = localStorage.getItem("theme-preference");
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const theme = stored === "dark" || (stored === "system" && systemDark) || (!stored && systemDark) ? "dark" : "light";
document.documentElement.classList.add(theme);
document.documentElement.style.colorScheme = theme;
})();
`,
}}
/>
</head>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}
```
## Tailwind Dark Mode Config
```javascript
// tailwind.config.js
module.exports = {
darkMode: "class",
theme: {
extend: {
colors: {
background: "var(--bg-primary)",
foreground: "var(--text-primary)",
},
},
},
};
```
## Best Practices
1. **Flash Prevention**: Use inline script to prevent flash of wrong theme on page load
2. **System Preference**: Respect user system preference as default
3. **Persistence**: Store user preference in localStorage
4. **Smooth Transitions**: Add CSS transitions for theme changes
5. **Accessibility**: Ensure sufficient contrast ratios in both themesThis dark-mode prompt is ideal for developers working on:
By using this prompt, you can save hours of manual coding and ensure best practices are followed from the start. It's particularly valuable for teams looking to maintain consistency across their dark-mode implementations.
Yes! All prompts on Antigravity AI Directory are free to use for both personal and commercial projects. No attribution required, though it's always appreciated.
This prompt works excellently with Claude, ChatGPT, Cursor, GitHub Copilot, and other modern AI coding assistants. For best results, use models with large context windows.
You can modify the prompt by adding specific requirements, constraints, or preferences. For dark-mode projects, consider mentioning your framework version, coding style, and any specific libraries you're using.