Theme provider with light/dark/system modes and optional DB persistence
npm install @nextdevx/themeLightweight theme provider for Next.js applications supporting light, dark, and system modes with localStorage persistence and optional database sync.
- Light/Dark/System Modes - Full support for light, dark, and system-preference themes
- localStorage Persistence - Theme preference saved locally for instant restoration
- System Theme Detection - Automatically detect and respond to OS theme changes
- Profile Sync - Optional sync with user profile in database
- SSR Safe - Proper hydration handling to prevent flash of wrong theme
- Zero Dependencies - Only requires React and lucide-react for icons
``bash`
npm install @nextdevx/themeor
pnpm add @nextdevx/themeor
yarn add @nextdevx/theme
`tsx
// app/layout.tsx
import { ThemeProvider } from '@nextdevx/theme'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
$3
Ensure your CSS supports dark mode. With Tailwind CSS:
`css
/ globals.css /
@tailwind base;
@tailwind components;
@tailwind utilities;/ Tailwind handles dark: automatically via class /
`Or with custom CSS:
`css
:root {
--background: #ffffff;
--foreground: #000000;
}.dark {
--background: #000000;
--foreground: #ffffff;
}
body {
background-color: var(--background);
color: var(--foreground);
}
`$3
`tsx
import { ThemeToggle } from '@nextdevx/theme'export function Header() {
return (
)
}
`API Reference
$3
Root provider for theme management.
`tsx
import { ThemeProvider } from '@nextdevx/theme' defaultTheme="system"
storageKey="my-app-theme"
enableSystemTheme={true}
onThemeChange={(theme) => console.log('Theme changed:', theme)}
initialTheme={userProfile?.theme}
>
{children}
`#### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
|
children | ReactNode | required | Child components |
| defaultTheme | Theme | 'system' | Default theme when no preference is saved |
| storageKey | string | 'nextstack-theme' | localStorage key for persistence |
| enableSystemTheme | boolean | true | Allow system theme option |
| onThemeChange | (theme: Theme) => void | - | Callback when theme changes |
| initialTheme | Theme | - | Initial theme from server (e.g., user profile) |$3
Pre-built theme toggle button.
`tsx
import { ThemeToggle } from '@nextdevx/theme'// Icon button (cycles through themes on click)
// With label
// Without system option
`#### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
|
variant | 'icon' \| 'dropdown' | 'icon' | Toggle style |
| showSystemOption | boolean | true | Include system theme in cycle |
| className | string | '' | Additional CSS classes |The toggle cycles through themes: light → dark → system → light (or light → dark → light if
showSystemOption is false).$3
Hook to access theme context.
`typescript
import { useTheme } from '@nextdevx/theme'function MyComponent() {
const { theme, resolvedTheme, setTheme } = useTheme()
// theme: 'light' | 'dark' | 'system'
// resolvedTheme: 'light' | 'dark' (actual computed value)
// setTheme: function to change theme
return (
Current setting: {theme}
Actual theme: {resolvedTheme}
)
}
`#### Return Value
`typescript
interface ThemeContextValue {
/* Current theme setting ('light', 'dark', or 'system') /
theme: Theme
/* Resolved theme value ('light' or 'dark') /
resolvedTheme: ResolvedTheme
/* Function to change the theme /
setTheme: (theme: Theme) => void
}
`$3
Component for syncing theme with user profile in database.
`tsx
import { ProfileThemeSync } from '@nextdevx/theme'function MyProfileThemeSync() {
const { user } = useAuth()
const { data: profile, isSuccess } = useProfile()
const updateTheme = async (theme: Theme) => {
await fetch('/api/profile/theme', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ theme }),
})
}
return (
user={user}
profileTheme={profile?.themePreference}
onProfileThemeUpdate={updateTheme}
isProfileLoaded={isSuccess}
/>
)
}
`#### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
|
user | { id: string } \| null | required | Current user object |
| profileTheme | Theme \| null | required | Theme from user profile |
| onProfileThemeUpdate | (theme: Theme) => void | required | Callback to update profile |
| isProfileLoaded | boolean | true | Whether profile data is loaded |#### Behavior
1. On login: Syncs profile theme → local state
2. On theme change: Updates profile (if logged in)
3. On logout: Resets sync state
Types
`typescript
type Theme = 'light' | 'dark' | 'system'
type ResolvedTheme = 'light' | 'dark'interface ThemeProviderProps {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
enableSystemTheme?: boolean
onThemeChange?: (theme: Theme) => void
initialTheme?: Theme
}
interface ThemeContextValue {
theme: Theme
resolvedTheme: ResolvedTheme
setTheme: (theme: Theme) => void
}
interface ProfileThemeSyncProps {
user: { id: string } | null | undefined
profileTheme: Theme | null | undefined
onProfileThemeUpdate: (theme: Theme) => void
isProfileLoaded?: boolean
}
`Usage Examples
$3
`tsx
import { useTheme } from '@nextdevx/theme'
import { Sun, Moon, Monitor } from 'lucide-react'function CustomToggle() {
const { theme, setTheme } = useTheme()
return (
onClick={() => setTheme('light')}
className={theme === 'light' ? 'bg-blue-500 text-white' : ''}
>
onClick={() => setTheme('dark')}
className={theme === 'dark' ? 'bg-blue-500 text-white' : ''}
>
onClick={() => setTheme('system')}
className={theme === 'system' ? 'bg-blue-500 text-white' : ''}
>
)
}
`$3
`tsx
// app/layout.tsx
import { cookies } from 'next/headers'
import { ThemeProvider } from '@nextdevx/theme'export default async function RootLayout({ children }) {
const cookieStore = await cookies()
const themeCookie = cookieStore.get('theme')?.value as Theme | undefined
return (
{children}
)
}
`$3
`tsx
import { useTheme } from '@nextdevx/theme'function Logo() {
const { resolvedTheme } = useTheme()
return (
src={resolvedTheme === 'dark' ? '/logo-white.svg' : '/logo-black.svg'}
alt="Logo"
/>
)
}
`$3
`tsx
import { ProfileThemeSync } from '@nextdevx/theme'
import { useAuth } from '@/hooks/useAuth'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'function ProfileThemeSyncWrapper() {
const { user } = useAuth()
const queryClient = useQueryClient()
const { data: profile, isSuccess } = useQuery({
queryKey: ['profile'],
queryFn: () => fetch('/api/profile').then(r => r.json()),
enabled: !!user,
})
const mutation = useMutation({
mutationFn: (theme: Theme) =>
fetch('/api/profile/theme', {
method: 'PATCH',
body: JSON.stringify({ theme }),
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['profile'] })
},
})
return (
user={user}
profileTheme={profile?.themePreference}
onProfileThemeUpdate={mutation.mutate}
isProfileLoaded={isSuccess}
/>
)
}
`Flash Prevention
To prevent a flash of wrong theme on page load, add
suppressHydrationWarning to your tag and consider adding an inline script:`tsx
// app/layout.tsx
export default function RootLayout({ children }) {
return (
dangerouslySetInnerHTML={{
__html: ,
}}
/>
{children}
)
}
`Peer Dependencies
| Package | Version | Required |
|---------|---------|----------|
|
react | >=18.0.0 | Yes |
| lucide-react | >=0.300.0 | Yes |TypeScript
All exports are fully typed:
`typescript
import type {
Theme,
ResolvedTheme,
ThemeProviderProps,
ThemeContextValue,
ProfileThemeSyncProps,
} from '@nextdevx/theme'
``MIT