@novakit-app/theme
v1.0.35
Published
Theme system for React Native UI Kit
Readme
@novakit-app/theme
A comprehensive theme management system for React Native applications that
integrates seamlessly with @novakit-app/foundation. Provides light/dark mode
support, persistent theme storage, and dynamic theme switching.
Features
- 🌓 Light/Dark Mode: Built-in light and dark theme variants
- 💾 Persistent Storage: Optional AsyncStorage integration for theme persistence
- 🔄 System Integration: Automatic system theme detection and following
- 🎨 Custom Themes: Easy theme customization and extension
- ⚡ Performance: Optimized with React.memo and useMemo
- 🔧 TypeScript: Full TypeScript support with comprehensive types
- 📱 React Native: Works with React Native, Expo, and React Native Web
Installation
npm install @novakit-app/themeNote: This package works with @novakit-app/foundation for styling. Install
both for the complete experience:
npm install @novakit-app/foundation @novakit-app/themeSetup Babel Plugin (for className support):
Add to your babel.config.js:
module.exports = {
plugins: [["@novakit-app/foundation/babel-plugin"]],
};For theme persistence (optional):
npm install @react-native-async-storage/async-storagePackage Separation
@novakit-app/theme provides:
ThemeProvider- React context for theme managementuseTheme()- Hooks for accessing theme statelightTheme,darkTheme- Pre-built theme configurations- Theme switching, persistence, and customization utilities
@novakit-app/foundation provides:
parseClassNames()- Style compiler for className stringsdefaultTheme- Base design tokens- Babel plugin for compile-time className transformation
- All styling utilities and design system tokens
- Theme utilities like
resolveColor(),getSpacing(), etc.
They work together: Theme provides the theme context, Foundation provides the styling engine.
Quick Start
1. Install Dependencies
npm install @novakit-app/foundation @novakit-app/theme2. Setup Babel Plugin (for className support)
Add to your babel.config.js:
module.exports = {
plugins: [["@novakit-app/foundation/babel-plugin"]],
};3. Wrap your app with ThemeProvider
import React from "react";
import { ThemeProvider } from "@novakit-app/theme";
export default function App() {
return (
<ThemeProvider
defaultMode="light" // Initial theme: "light" or "dark"
followSystem={true} // Follow system theme changes
persistMode={true} // Save user's theme preference
>
<YourApp />
</ThemeProvider>
);
}Light/Dark Theme Setup
The ThemeProvider automatically handles light and dark themes. Here's how it works:
Default Theme Behavior
import { ThemeProvider } from "@novakit-app/theme";
// Option 1: Use built-in light/dark themes (recommended)
<ThemeProvider
defaultMode="light"
followSystem={true}
>
<App />
</ThemeProvider>;What happens:
defaultMode="light"- Starts with light themefollowSystem={true}- Automatically switches when user changes system theme- Built-in
lightThemeanddarkThemeare used automatically
Mode-Based Custom Themes (Recommended)
import { ThemeProvider } from "@novakit-app/theme";
// ✅ Define both light and dark themes in one object
const customTheme = {
light: {
colors: {
primary: "#1da1f2", // Twitter blue for light mode
background: "#ffffff",
surface: "#f8f9fa",
text: "#14171a",
},
},
dark: {
colors: {
primary: "#00d4ff", // Cyan for dark mode
background: "#000000",
surface: "#1a1a1a",
text: "#ffffff",
},
},
};
<ThemeProvider
customTheme={customTheme}
defaultMode="light"
followSystem={true}
>
<App />
</ThemeProvider>;What happens:
- When mode is "light" → uses
customTheme.light - When mode is "dark" → uses
customTheme.dark - Automatically switches themes when mode changes
- Foundation compiler uses the current mode-based theme
Traditional Custom Themes
import { ThemeProvider } from "@novakit-app/theme";
import { createLightTheme, createDarkTheme } from "@novakit-app/theme";
// Create separate themes (legacy approach)
const customLightTheme = createLightTheme({
colors: {
primary: "#1da1f2",
background: "#ffffff",
surface: "#f8f9fa",
text: "#14171a",
},
});
const customDarkTheme = createDarkTheme({
colors: {
primary: "#1da1f2", // Same blue for consistency
background: "#000000",
surface: "#16181c",
text: "#ffffff",
},
});
// Use custom theme (applies to both modes)
<ThemeProvider
defaultMode="light"
customTheme={customLightTheme}
>
<App />
</ThemeProvider>;Partial Mode Themes
import { ThemeProvider } from "@novakit-app/theme";
// ✅ Only customize light mode, use default dark theme
const customTheme = {
light: {
colors: {
primary: "#1da1f2",
background: "#ffffff",
surface: "#f8f9fa",
},
},
// No dark theme - will use default darkTheme
};
<ThemeProvider customTheme={customTheme}>
<App />
</ThemeProvider>;Regular Custom Theme (Both Modes)
import { ThemeProvider } from "@novakit-app/theme";
// ✅ Same theme for both light and dark modes
const customTheme = {
colors: {
primary: "#1da1f2", // Same color for both modes
secondary: "#657786",
},
spacing: {
4: 20,
8: 40,
},
};
<ThemeProvider customTheme={customTheme}>
<App />
</ThemeProvider>;ThemeProvider Configuration Options
Basic Configuration
import { ThemeProvider } from "@novakit-app/theme";
<ThemeProvider
defaultMode="light" // "light" | "dark" - Initial theme
followSystem={true} // Follow system theme changes
persistMode={false} // Save theme preference to storage
storageKey="@myapp/theme" // Custom storage key
>
<App />
</ThemeProvider>;Configuration Examples
// Example 1: Simple setup with system following
<ThemeProvider
defaultMode="light"
followSystem={true}
>
<App />
</ThemeProvider>
// Example 2: With persistence
<ThemeProvider
defaultMode="dark"
followSystem={true}
persistMode={true}
storageKey="@myapp/theme-mode"
>
<App />
</ThemeProvider>
// Example 3: Custom theme with overrides
<ThemeProvider
defaultMode="light"
customTheme={{
colors: {
primary: "#1da1f2",
secondary: "#657786",
}
}}
>
<App />
</ThemeProvider>
// Example 4: Manual control (no system following)
<ThemeProvider
defaultMode="light"
followSystem={false}
persistMode={true}
>
<App />
</ThemeProvider>Understanding the Props
| Prop | Type | Default | Description |
| -------------- | ---------------------------------------- | ----------------------- | -------------------------------- |
| defaultMode | "light" \| "dark" | "light" | Initial theme mode |
| followSystem | boolean | true | Follow system theme changes |
| persistMode | boolean | false | Save theme preference to storage |
| storageKey | string | "@novakit/theme/mode" | Storage key for persistence |
| customTheme | Partial<ThemeConfig> \| ModeBasedTheme | undefined | Custom theme overrides |
customTheme Types:
Partial<ThemeConfig>- Regular theme overrides for both modesModeBasedTheme- Separate themes for light and dark modes
Theme Behavior Flow
// 1. App starts
<ThemeProvider
defaultMode="light"
followSystem={true}
persistMode={true}
>
<App />
</ThemeProvider>
// 2. ThemeProvider checks:
// - Is there a saved preference? (if persistMode=true)
// - What's the system theme? (if followSystem=true)
// - Use defaultMode as fallback
// 3. User changes system theme (if followSystem=true)
// - Theme automatically switches
// - Preference is saved (if persistMode=true)
// 4. User manually toggles theme
// - Theme switches immediately
// - System following is disabled
// - Preference is saved (if persistMode=true)4. Use className in your components
import React from "react";
import { View, Text } from "react-native";
import { useTheme } from "@novakit-app/theme";
export function MyComponent() {
const { mode, toggleMode, isLight } = useTheme();
return (
<View className="p-4 bg-primary rounded-lg">
<Text className="text-lg text-white">
Current mode: {mode}
</Text>
<Text onPress={toggleMode}>
Toggle to {isLight ? "dark" : "light"} mode
</Text>
</View>
);
}API Reference
ThemeProvider Props
interface ThemeProviderProps {
children: React.ReactNode;
customTheme?: Partial<ThemeConfig> | ModeBasedTheme;
defaultMode?: "light" | "dark";
followSystem?: boolean;
persistMode?: boolean;
storageKey?: string;
}
interface ModeBasedTheme {
light?: Partial<ThemeConfig>;
dark?: Partial<ThemeConfig>;
}customTheme: Custom theme overrides to apply (supports mode-based themes)defaultMode: Initial theme mode (default: "light")followSystem: Follow system theme changes (default: true)persistMode: Save theme mode to storage (default: false)storageKey: Storage key for persistence (default: "@novakit/theme/mode")
useTheme Hook
const {
theme, // Current theme configuration
mode, // Current mode: "light" | "dark"
setMode, // Function to set specific mode
toggleMode, // Function to toggle between modes
setTheme, // Function to update theme (limited)
isSystemMode, // Whether following system theme
isLight, // Boolean: is light mode
isDark, // Boolean: is dark mode
} = useTheme();Additional Hooks
// Get only the theme config
const theme = useThemeConfig();
// Get only the mode and mode helpers
const { mode, isLight, isDark } = useThemeMode();
// Get only the control functions
const { setMode, toggleMode, setTheme } = useThemeControls();Theme Customization
Mode-Based Custom Themes (Recommended)
The easiest way to customize themes is using mode-based custom themes:
import { ThemeProvider } from "@novakit-app/theme";
const customTheme = {
light: {
colors: {
primary: "#1da1f2", // Twitter blue for light mode
background: "#ffffff",
surface: "#f8f9fa",
text: "#14171a",
},
spacing: {
4: 20,
8: 40,
},
},
dark: {
colors: {
primary: "#00d4ff", // Cyan for dark mode
background: "#000000",
surface: "#1a1a1a",
text: "#ffffff",
},
spacing: {
4: 20,
8: 40,
},
},
};
<ThemeProvider customTheme={customTheme}>
<App />
</ThemeProvider>;Benefits:
- Define both light and dark themes in one object
- Automatic theme switching based on mode
- Foundation compiler automatically uses the current theme
- Type-safe with
ModeBasedThemeinterface
Regular Custom Theme Overrides
import { ThemeProvider } from "@novakit-app/theme";
// Same theme applied to both light and dark modes
const customTheme = {
colors: {
primary: "#1da1f2",
secondary: "#657786",
},
spacing: {
1: 2,
2: 4,
},
};
<ThemeProvider customTheme={customTheme}>
<App />
</ThemeProvider>;Create Custom Themes
import {
createLightTheme,
createDarkTheme,
extendTheme,
} from "@novakit-app/theme";
// Create a custom light theme
const myLightTheme = createLightTheme({
colors: {
primary: "#1da1f2",
brand: {
50: "#eef6ff",
500: "#1da1f2",
900: "#0a1a3f",
},
},
});
// Create a custom dark theme
const myDarkTheme = createDarkTheme({
colors: {
primary: "#1da1f2",
background: "#0a0a0a",
},
});
// Extend existing themes
const extendedTheme = extendTheme(myLightTheme, {
typography: {
fontSize: {
"2xs": 10,
"3xl": 32,
},
},
});Theme with Color Palette
import { createThemeWithPalette } from "@novakit-app/theme";
const palette = {
brand: {
50: "#eef6ff",
100: "#dbeafe",
500: "#3b82f6",
900: "#1e3a8a",
},
accent: "#f59e0b",
};
const theme = createThemeWithPalette(palette);Advanced Usage
Custom Storage Implementation
import { ThemeProvider } from "@novakit-app/theme";
class CustomStorage {
async getItem(key: string) {
// Your custom storage logic
return localStorage.getItem(key);
}
async setItem(key: string, value: string) {
localStorage.setItem(key, value);
}
async removeItem(key: string) {
localStorage.removeItem(key);
}
}
<ThemeProvider
storage={new CustomStorage()}
storageKey="my-app-theme"
>
<App />
</ThemeProvider>;Conditional Theme Application
import { useTheme } from "@novakit-app/theme";
function ConditionalComponent() {
const { theme, isDark } = useTheme();
const containerStyle = {
backgroundColor: isDark
? theme.colors.surface
: theme.colors.background,
padding: theme.spacing[4],
};
return <View style={containerStyle} />;
}Theme-Aware Component
import React from "react";
import { View, Text } from "react-native";
interface ThemedCardProps {
title: string;
children: React.ReactNode;
}
export function ThemedCard({ title, children }: ThemedCardProps) {
return (
<View className="p-4 bg-surface rounded-lg shadow-md">
<Text className="text-lg font-semibold text-text mb-2">
{title}
</Text>
{children}
</View>
);
}Mode-Based Theme Examples
Complete App Example
import React from "react";
import { View, Text, TouchableOpacity } from "react-native";
import { ThemeProvider, useTheme } from "@novakit-app/theme";
// Define mode-based custom theme
const appTheme = {
light: {
colors: {
primary: "#1da1f2", // Twitter blue
secondary: "#657786",
background: "#ffffff",
surface: "#f8f9fa",
text: "#14171a",
textSecondary: "#657786",
},
},
dark: {
colors: {
primary: "#00d4ff", // Cyan
secondary: "#8899a6",
background: "#000000",
surface: "#1a1a1a",
text: "#ffffff",
textSecondary: "#8899a6",
},
},
};
function App() {
return (
<ThemeProvider
customTheme={appTheme}
defaultMode="light"
followSystem={true}
persistMode={true}
>
<MainScreen />
</ThemeProvider>
);
}
function MainScreen() {
const { mode, toggleMode, isLight } = useTheme();
return (
<View className="flex-1 bg-background">
<View className="p-6">
<Text className="text-2xl font-bold text-text mb-4">
Theme Demo
</Text>
<Text className="text-textSecondary mb-6">
Current mode: {mode}
</Text>
<TouchableOpacity
className="bg-primary p-4 rounded-lg mb-6"
onPress={toggleMode}
>
<Text className="text-white text-center font-semibold">
Switch to {isLight ? "Dark" : "Light"}{" "}
Mode
</Text>
</TouchableOpacity>
{/* Color palette demonstration */}
<View className="space-y-4">
<View className="bg-primary p-4 rounded-lg">
<Text className="text-white">
Primary Color
</Text>
</View>
<View className="bg-secondary p-4 rounded-lg">
<Text className="text-white">
Secondary Color
</Text>
</View>
<View className="bg-surface p-4 rounded-lg border border-gray-200">
<Text className="text-text">
Surface Color
</Text>
</View>
</View>
</View>
</View>
);
}Dynamic Theme Creation
import { ThemeProvider } from "@novakit-app/theme";
function createBrandTheme(brandColor: string) {
return {
light: {
colors: {
primary: brandColor,
background: "#ffffff",
text: "#000000",
surface: "#f8f9fa",
},
},
dark: {
colors: {
primary: brandColor,
background: "#000000",
text: "#ffffff",
surface: "#1a1a1a",
},
},
};
}
function App() {
const brandTheme = createBrandTheme("#1da1f2");
return (
<ThemeProvider customTheme={brandTheme}>
<MyApp />
</ThemeProvider>
);
}How Theme Switching Works with Foundation
The theme package automatically provides the current theme to foundation's styling system. Here's how it works:
Automatic Theme Integration
import { useTheme } from "@novakit-app/theme";
import { resolveColor, getSpacing } from "@novakit-app/foundation";
function MyComponent() {
const { theme, mode, toggleMode } = useTheme();
return (
<View className="p-4 bg-primary rounded-lg">
<Text className="text-white text-lg">
Current mode: {mode}
</Text>
<Text
className="text-white underline mt-2"
onPress={toggleMode}
>
Switch to {mode === "light" ? "dark" : "light"} mode
</Text>
</View>
);
}What happens when you toggle:
toggleMode()changes the theme mode- ThemeProvider updates the theme object
- All
classNamestyles automatically use the new theme - Foundation's compiler resolves colors using the current theme
Manual Theme Integration
import { useTheme } from "@novakit-app/theme";
import { resolveColor, getSpacing } from "@novakit-app/foundation";
function ManualThemeComponent() {
const { theme, mode } = useTheme();
// ✅ Foundation utilities automatically use current theme
const primaryColor = resolveColor("primary", mode, theme);
const padding = getSpacing(4, theme);
const borderRadius = theme.borderRadius.lg;
return (
<View
style={{
backgroundColor: primaryColor,
padding,
borderRadius,
}}
>
<Text style={{ color: theme.colors.white }}>
Manual theme integration - Mode: {mode}
</Text>
</View>
);
}Theme-Aware Component Example
import React from "react";
import { View, Text, TouchableOpacity } from "react-native";
import { useTheme } from "@novakit-app/theme";
export function ThemeToggleCard() {
const { mode, toggleMode, isLight, isDark } = useTheme();
return (
<View className="p-6 bg-surface rounded-xl shadow-lg mx-4">
<Text className="text-xl font-bold text-text mb-4">
Theme Settings
</Text>
<Text className="text-textSecondary mb-4">
Current theme: {mode}
</Text>
<TouchableOpacity
className="bg-primary p-4 rounded-lg"
onPress={toggleMode}
>
<Text className="text-white text-center font-semibold">
Switch to {isLight ? "Dark" : "Light"} Mode
</Text>
</TouchableOpacity>
{/* These colors automatically change with theme */}
<View className="flex-row mt-4 space-x-2">
<View className="w-8 h-8 bg-primary rounded" />
<View className="w-8 h-8 bg-secondary rounded" />
<View className="w-8 h-8 bg-success rounded" />
<View className="w-8 h-8 bg-danger rounded" />
</View>
</View>
);
}Foundation Integration Deep Dive
import { useTheme } from "@novakit-app/theme";
import {
resolveColor,
getSpacing,
getFontSize,
parseClassNamesWithTheme,
} from "@novakit-app/foundation";
function AdvancedThemeComponent() {
const { theme, mode } = useTheme();
// ✅ All foundation utilities work with current theme
const styles = {
container: {
backgroundColor: resolveColor("surface", mode, theme),
padding: getSpacing(4, theme),
borderRadius: theme.borderRadius.lg,
},
text: {
color: resolveColor("text", mode, theme),
fontSize: getFontSize("lg", theme),
},
button: {
backgroundColor: resolveColor("primary", mode, theme),
paddingHorizontal: getSpacing(6, theme),
paddingVertical: getSpacing(3, theme),
},
};
// ✅ Or use className with theme context
const classNameStyles = parseClassNamesWithTheme(
"p-4 bg-primary rounded-lg text-white",
theme
);
return (
<View style={styles.container}>
<Text style={styles.text}>Advanced theme integration</Text>
<View style={styles.button}>
<Text style={{ color: "white" }}>Button</Text>
</View>
</View>
);
}Best Practices
- Wrap your app early: Place
ThemeProviderat the root of your app - Use theme tokens: Prefer theme tokens over hardcoded values
- Leverage system integration: Use
followSystem={true}for better UX - Persist user preference: Enable
persistModefor user convenience - Custom themes sparingly: Only override what you need to change
- Performance: The theme is memoized, but avoid creating new objects in render
Migration from Basic Theme
If you're migrating from a basic theme setup:
// Before
const theme = { colors: { primary: "#007bff" } };
// After
import { ThemeProvider } from "@novakit-app/theme";
<ThemeProvider customTheme={{ colors: { primary: "#007bff" } }}>
<App />
</ThemeProvider>;Troubleshooting
Theme not updating
- Ensure
ThemeProviderwraps your component tree - Check that you're using
useTheme()hook correctly - Verify theme overrides are properly structured
Storage issues
- AsyncStorage is optional; the package works without it
- Check storage permissions on your platform
- Use custom storage implementation if needed
Performance issues
- Theme is memoized, but custom theme objects should be stable
- Avoid creating new theme objects in render
- Use
useThemeConfig()if you only need the theme object
TypeScript Support
Full TypeScript support with comprehensive types:
import type {
ThemeConfig,
ThemeMode,
ThemeProviderProps,
ModeBasedTheme,
ThemeContextType,
UseThemeReturn,
} from "@novakit-app/theme";
// Type-safe mode-based theme
const customTheme: ModeBasedTheme = {
light: {
colors: { primary: "#1da1f2" },
},
dark: {
colors: { primary: "#00d4ff" },
},
};
// Type-safe theme provider props
const themeProps: ThemeProviderProps = {
customTheme,
defaultMode: "light",
followSystem: true,
persistMode: true,
};Quick Reference
Mode-Based Theme Setup
// ✅ Recommended: Mode-based custom theme
const customTheme = {
light: { colors: { primary: "#1da1f2" } },
dark: { colors: { primary: "#00d4ff" } },
};
<ThemeProvider customTheme={customTheme}>
<App />
</ThemeProvider>;Usage in Components
// ✅ Automatic theme integration
<View className="p-4 bg-primary rounded-lg">
<Text className="text-white">Uses current theme</Text>
</View>;
// ✅ Manual theme access
const { theme, mode, toggleMode } = useTheme();
const primaryColor = theme.colors.primary;Key Benefits
- Mode-Based Themes: Define light and dark themes in one object
- Automatic Switching: Themes switch automatically when mode changes
- Foundation Integration: Compiler automatically uses current theme
- Type Safety: Full TypeScript support with
ModeBasedThemeinterface - Backward Compatible: Regular custom themes still work
License
MIT
