This chat UI application includes comprehensive dark theme support built with Next.js 15, React 18, and next-themes. The implementation provides a seamless user experience with system theme detection and manual controls.
- System Theme Detection: Automatically detects and follows the user's system theme preference (default)
- Manual Theme Toggle: Users can manually switch between light and dark themes
- Smooth Transitions: CSS transitions provide smooth theme switching animations (0.3s ease)
- Persistent Theme: Theme preference is saved and restored across sessions using localStorage
- SSR Safe: Prevents hydration mismatches with proper mounting checks
- Accessibility: Full ARIA support, screen reader compatibility, and keyboard navigation
Wraps the entire application with theme context using next-themes. Configured in app/layout.tsx with:
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>Key Configuration:
attribute="class": Uses CSS classes for theme switchingdefaultTheme="system": Follows system preference by defaultenableSystem: Enables system theme detectiondisableTransitionOnChange: Prevents flash during theme changes
A minimal toggle button that switches between light and dark themes with animated sun/moon icons:
- ☀️ Sun icon for light theme (visible in light mode)
- 🌙 Moon icon for dark theme (visible in dark mode)
- Smooth rotation and scale animations
- Ghost button variant for subtle appearance
A dropdown menu that allows selection between light and dark themes:
- Shows current theme in button label
- Dropdown menu with explicit theme options
- Proper hydration handling to prevent SSR mismatches
- Accessible with proper ARIA labels
Note: The current implementation only supports light and dark themes (no system option in dropdown).
The application uses CSS custom properties defined in app/globals.css for consistent theming:
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}Dark mode is configured in tailwind.config.js:
module.exports = {
darkMode: ["class"], // Uses class-based dark mode
theme: {
extend: {
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
// ... other color mappings
},
},
},
plugins: [require("tailwindcss-animate")],
};The application includes smooth transitions for theme changes:
body {
@apply bg-background text-foreground;
transition:
background-color 0.3s ease,
color 0.3s ease;
}
.theme-transition {
transition:
background-color 0.3s ease,
color 0.3s ease,
border-color 0.3s ease;
}- System Theme (Default): Automatically follows your OS dark/light mode preference
- Manual Toggle: Click the theme toggle button in the chat interface
- Theme Persistence: Your preference is saved and restored across sessions
import { useTheme } from "next-themes";
function MyComponent() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Current theme: {theme}
</button>
);
}The theme components handle SSR properly to prevent hydration mismatches:
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return <LoadingState />;
}- System Theme: Uses
prefers-color-schememedia query - Manual Override: Stored in localStorage as
themekey - Class Application: Automatically applies
.darkclass to<html>element
- Add to both
:rootand.darkselectors inapp/globals.css - Map to Tailwind colors in
tailwind.config.js - Use with
hsl(var(--your-variable))format
:root {
--custom-color: 210 40% 50%;
}
.dark {
--custom-color: 210 40% 80%;
}// tailwind.config.js
colors: {
custom: "hsl(var(--custom-color))",
}- ARIA Labels: All theme controls have descriptive labels
- Screen Reader Support: Hidden text for assistive technologies
- Keyboard Navigation: Full keyboard accessibility support
- High Contrast: Proper contrast ratios in both themes
- Focus Indicators: Visible focus states for keyboard users
{
"next-themes": "^0.4.6",
"lucide-react": "^0.294.0",
"@radix-ui/react-dropdown-menu": "^2.0.6"
}- Always use CSS variables for colors that need to change between themes
- Test both themes during development
- Consider system theme as the default for better UX
- Use semantic color names (primary, secondary, muted) rather than specific colors
- Ensure proper contrast ratios for accessibility compliance