Skip to content

Theme Providers

Sparkle provides two specialized theme providers to manage theme state and make design tokens available throughout your application:

  • ThemeProvider - For web applications using React DOM
  • NativeThemeProvider - For React Native mobile applications

Both providers offer the same core functionality but with platform-specific optimizations and features.

Both theme providers provide:

  • Theme switching - Change between light, dark, and system themes
  • Persistence - Remember user’s theme preference across sessions
  • System detection - Automatically follow system color scheme
  • Validation - Ensure theme configurations are valid
  • Error handling - Graceful fallbacks when themes fail to load
  • Type safety - Full TypeScript support with design token types

Web (ThemeProvider):

  • CSS custom properties injection
  • localStorage persistence
  • matchMedia system detection
  • SSR/hydration support

React Native (NativeThemeProvider):

  • AsyncStorage persistence
  • Appearance API system detection
  • StatusBar style integration
  • Platform-specific fallbacks
import {ThemeProvider} from '@sparkle/theme'
function App() {
return (
<ThemeProvider defaultTheme="system">
<YourAppContent />
</ThemeProvider>
)
}
import {darkTokens, lightTokens, ThemeProvider} from '@sparkle/theme'
function App() {
return (
<ThemeProvider
// Default theme mode on first load
defaultTheme="light"
// Custom theme configurations
themes={{
light: lightTokens,
dark: darkTokens,
}}
// Persistence settings
storageKey="my-app-theme"
// System theme detection
disableSystemTheme={false}
// CSS variable injection target
cssSelector=":root"
>
<YourAppContent />
</ThemeProvider>
)
}
interface ThemeProviderProps {
/** Child components that will have access to the theme context */
children: ReactNode
/** Default theme mode to use on first load (default: 'system') */
defaultTheme?: ThemeMode
/** Custom theme configurations to override defaults */
themes?: ThemeCollection
/** Storage key for persisting theme preference (default: 'sparkle-theme') */
storageKey?: string
/** Whether to disable system theme detection (default: false) */
disableSystemTheme?: boolean
/** CSS selector for injecting CSS variables (default: ':root') */
cssSelector?: string
}

When you use ThemeProvider, CSS custom properties are automatically injected into the DOM:

/* Automatically generated by ThemeProvider */
:root {
--color-primary-500: #3b82f6;
--color-secondary-500: #64748b;
--spacing-md: 1rem;
--font-size-base: 1rem;
/* ... all theme tokens */
}
/* In dark mode */
:root {
--color-primary-500: #60a5fa;
--color-secondary-500: #94a3b8;
/* ... dark theme tokens */
}

Use these variables directly in your CSS:

.button {
background-color: var(--color-primary-500);
color: var(--color-text-inverse);
padding: var(--spacing-md);
border-radius: var(--border-radius-md);
}

The ThemeProvider handles SSR scenarios gracefully:

// Next.js example
import {ThemeProvider} from '@sparkle/theme'
function MyApp({Component, pageProps}) {
return (
<ThemeProvider defaultTheme="light">
<Component {...pageProps} />
</ThemeProvider>
)
}
// The provider will:
// 1. Use defaultTheme during SSR
// 2. Hydrate with stored preference on client
// 3. Prevent hydration mismatches
import {NativeThemeProvider} from '@sparkle/theme'
function App() {
return (
<NativeThemeProvider defaultTheme="system">
<YourAppContent />
</NativeThemeProvider>
)
}
import {darkTokens, lightTokens, NativeThemeProvider} from '@sparkle/theme'
function App() {
return (
<NativeThemeProvider
// Default theme mode on first load
defaultTheme="light"
// Custom theme configurations
themes={{
light: lightTokens,
dark: darkTokens,
}}
// Persistence settings
storageKey="my-app-theme"
// System theme detection
disableSystemTheme={false}
// StatusBar integration
updateStatusBar={true}
>
<YourAppContent />
</NativeThemeProvider>
)
}
interface NativeThemeProviderProps {
/** Child components that will have access to the theme context */
children: ReactNode
/** Default theme mode to use on first load (default: 'system') */
defaultTheme?: ThemeMode
/** Custom theme configurations to override defaults */
themes?: ThemeCollection
/** Storage key for persisting theme preference (default: 'sparkle-theme') */
storageKey?: string
/** Whether to disable system theme detection (default: false) */
disableSystemTheme?: boolean
/** Whether to automatically update StatusBar style based on theme (default: true) */
updateStatusBar?: boolean
}

The NativeThemeProvider automatically manages StatusBar styling:

// Automatically handles StatusBar based on theme
import {StatusBar} from 'expo-status-bar'
function App() {
return (
<NativeThemeProvider>
{/* StatusBar style is automatically set based on theme */}
<StatusBar style="auto" />
<YourAppContent />
</NativeThemeProvider>
)
}
// Light theme: StatusBar style = 'dark'
// Dark theme: StatusBar style = 'light'

Theme preferences are automatically persisted using AsyncStorage:

// No additional setup required - persistence is automatic
import {NativeThemeProvider} from '@sparkle/theme'
function App() {
return (
<NativeThemeProvider storageKey="my-custom-key">
<YourAppContent />
</NativeThemeProvider>
)
}
// Theme changes are automatically saved to AsyncStorage
// On app restart, the last selected theme is restored

Both providers expose the same useTheme hook interface:

import {useTheme} from '@sparkle/theme'
function ThemedComponent() {
const {
theme, // Current theme configuration object
activeTheme, // Current theme mode: 'light' | 'dark' | 'system'
setTheme, // Function to change theme
systemTheme, // Detected system theme: 'light' | 'dark'
isLoading, // Loading state during initialization
error, // Error state if theme loading fails
} = useTheme()
// Access design tokens
const primaryColor = theme.colors.primary[500]
const spacing = theme.spacing.md
// Change theme
const switchToLight = () => setTheme('light')
const switchToDark = () => setTheme('dark')
const followSystem = () => setTheme('system')
if (isLoading) {
return <div>Loading theme...</div>
}
if (error) {
return <div>Error loading theme: {error.message}</div>
}
return (
<div style={{color: primaryColor}}>
<p>Current theme: {activeTheme}</p>
<p>System prefers: {systemTheme}</p>
<button onClick={switchToLight}>Light</button>
<button onClick={switchToDark}>Dark</button>
<button onClick={followSystem}>System</button>
</div>
)
}

The theme context provides the following interface:

interface ThemeContextValue {
/** Current theme configuration with all design tokens */
theme: ThemeConfig
/** Currently active theme mode */
activeTheme: ThemeMode
/** Function to change the theme */
setTheme: (theme: ThemeMode) => void | Promise<void>
/** System-detected color scheme preference */
systemTheme: SystemColorScheme
/** Whether the theme is currently loading */
isLoading: boolean
/** Error state if theme loading/validation fails */
error: Error | null
}

Define your own theme variants:

import {baseTokens, ThemeProvider} from '@sparkle/theme'
// Create custom theme variants
const customThemes = {
light: {
...baseTokens,
colors: {
...baseTokens.colors,
primary: {500: '#0066cc'}, // Custom blue
},
},
dark: {
...baseTokens,
colors: {
...baseTokens.colors,
primary: {500: '#3399ff'}, // Lighter blue for dark mode
background: {primary: '#1a1a1a'},
},
},
// Add more themes
highContrast: {
...baseTokens,
colors: {
...baseTokens.colors,
primary: {500: '#000000'},
background: {primary: '#ffffff'},
},
},
}
function App() {
return (
<ThemeProvider themes={customThemes} defaultTheme="light">
<YourAppContent />
</ThemeProvider>
)
}

Create reusable theme controls:

import {useTheme} from '@sparkle/theme'
function ThemeToggle() {
const {activeTheme, setTheme, systemTheme} = useTheme()
const cycleTheme = () => {
switch (activeTheme) {
case 'light':
setTheme('dark')
break
case 'dark':
setTheme('system')
break
case 'system':
setTheme('light')
break
}
}
const getButtonText = () => {
switch (activeTheme) {
case 'light':
return '☀️ Light'
case 'dark':
return '🌙 Dark'
case 'system':
return `🔄 System (${systemTheme})`
}
}
return <button onClick={cycleTheme}>{getButtonText()}</button>
}
function ThemeSelect() {
const {activeTheme, setTheme} = useTheme()
return (
<select value={activeTheme} onChange={(e) => setTheme(e.target.value as ThemeMode)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">Follow System</option>
</select>
)
}
import {useTheme} from '@sparkle/theme'
function ConditionalThemedComponent() {
const {activeTheme, systemTheme} = useTheme()
// Get the resolved theme (accounting for system preference)
const resolvedTheme = activeTheme === 'system' ? systemTheme : activeTheme
return (
<div>
{resolvedTheme === 'dark' ? (
<DarkModeSpecificComponent />
) : (
<LightModeSpecificComponent />
)}
{/* Show different icons based on theme */}
<img
src={resolvedTheme === 'dark' ? '/logo-dark.png' : '/logo-light.png'}
alt="Logo"
/>
</div>
)
}

Extract theme logic to share between web and React Native:

shared/theme.ts
import {baseTokens} from '@sparkle/theme'
export const appThemes = {
light: {
...baseTokens,
colors: {
...baseTokens.colors,
primary: {500: '#007AFF'}, // iOS blue
},
},
dark: {
...baseTokens,
colors: {
...baseTokens.colors,
primary: {500: '#0A84FF'}, // iOS dark blue
background: {primary: '#000000'},
},
},
}
// Custom hook for theme switching logic
export function useAppTheme() {
const {theme, setTheme, activeTheme} = useTheme()
const toggleTheme = () => {
setTheme(activeTheme === 'light' ? 'dark' : 'light')
}
return {theme, setTheme, activeTheme, toggleTheme}
}
web/App.tsx
import {ThemeProvider} from '@sparkle/theme'
import {appThemes} from '../shared/theme'
function WebApp() {
return (
<ThemeProvider themes={appThemes}>
<YourWebApp />
</ThemeProvider>
)
}
native/App.tsx
import {NativeThemeProvider} from '@sparkle/theme'
import {appThemes} from '../shared/theme'
function NativeApp() {
return (
<NativeThemeProvider themes={appThemes}>
<YourNativeApp />
</NativeThemeProvider>
)
}
// ✅ Good - Use platform-specific providers
// Web
import {ThemeProvider} from '@sparkle/theme'
// React Native
import {NativeThemeProvider} from '@sparkle/theme'
// ❌ Avoid - Don't mix platforms
// Don't use ThemeProvider in React Native
// Don't use NativeThemeProvider in web apps
import {useTheme} from '@sparkle/theme'
function App() {
const {isLoading, error, theme} = useTheme()
if (isLoading) {
return <LoadingScreen />
}
if (error) {
return <ErrorScreen error={error} />
}
return <MainApp theme={theme} />
}
import {useTheme} from '@sparkle/theme'
import {memo} from 'react'
// ✅ Good - Memoize components that use theme
const ThemedButton = memo(({children}) => {
const {theme} = useTheme()
return (
<button style={{backgroundColor: theme.colors.primary[500]}}>
{children}
</button>
)
})
// ✅ Good - Extract only what you need
function OptimizedComponent() {
const {setTheme} = useTheme() // Only subscribe to setTheme
return <button onClick={() => setTheme('dark')}>Dark Mode</button>
}
import {ThemeProvider, validateTheme} from '@sparkle/theme'
const customTheme = {
// ... your custom theme
}
// Validate before using
const validation = validateTheme(customTheme)
if (!validation.isValid) {
console.error('Theme validation failed:', validation.errors)
// Handle invalid theme
}
function App() {
return (
<ThemeProvider themes={{light: customTheme}}>
<YourApp />
</ThemeProvider>
)
}
import {ThemeProvider} from '@sparkle/theme'
function App() {
return (
<ThemeProvider
// Provide fallback theme
defaultTheme="light"
themes={{
light: safeTheme,
dark: safeTheme,
}}
>
<ErrorBoundary fallback={<ErrorUI />}>
<YourApp />
</ErrorBoundary>
</ThemeProvider>
)
}

Issue: Theme not persisting between sessions

Section titled “Issue: Theme not persisting between sessions”

Web:

// Check localStorage support
if (typeof localStorage !== 'undefined') {
console.log('Theme stored:', localStorage.getItem('sparkle-theme'))
}

React Native:

// Check AsyncStorage
import AsyncStorage from '@react-native-async-storage/async-storage'
AsyncStorage.getItem('sparkle-theme').then(value => {
console.log('Theme stored:', value)
})

Web:

// Check matchMedia support
if (window.matchMedia) {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
console.log('System prefers dark:', isDark)
}

React Native:

// Check Appearance API
import {Appearance} from 'react-native'
console.log('System color scheme:', Appearance.getColorScheme())
import {ThemeProvider} from '@sparkle/theme'
// Check if CSS variables are injected
// Force CSS variable update
const root = document.documentElement
const primaryColor = getComputedStyle(root).getPropertyValue('--color-primary-500')
console.log('Primary color CSS var:', primaryColor);
<ThemeProvider cssSelector=":root" forceUpdate={true}>
<App />
</ThemeProvider>
  1. Memoize theme-dependent components to prevent unnecessary re-renders
  2. Use CSS custom properties on web for better performance than inline styles
  3. Pre-load theme assets (like themed images) to prevent flash during theme switches
  4. Debounce rapid theme changes if allowing programmatic theme switching

For complete API documentation, see: