Colors
Primary, secondary, background, surface, text, and semantic colors for your brand.
Create unique, branded themes that perfectly match your application’s design requirements. RNC Theme provides powerful customization capabilities while maintaining type safety and performance.
Every custom theme follows a consistent structure with five main categories:
Colors
Primary, secondary, background, surface, text, and semantic colors for your brand.
Typography
Font sizes, line heights, and weights for consistent text styling.
Spacing
Consistent spacing values for margins, padding, and layout.
Components
Component-specific styling like heights, padding, and border radius.
Font Sizes
Scalable font size system for responsive typography.
const colors = (isDark: boolean) => { primary: isDark ? '#FF6B6B' : '#4ECDC4', secondary: isDark ? '#FFE66D' : '#45B7D1', background: isDark ? '#1a1a1a' : '#f8f9fa', surface: isDark ? '#2d2d2d' : '#ffffff', text: isDark ? '#ffffff' : '#333333', textSecondary: isDark ? '#b0b0b0' : '#666666', border: isDark ? '#404040' : '#e0e0e0', error: '#FF5252', warning: '#FF9800', success: '#4CAF50', info: '#2196F3', muted: isDark ? '#666666' : '#999999', accent: isDark ? '#FF6B6B' : '#4ECDC4', destructive: '#FF5252', }
const customComponents = { height: { xs: 32, sm: 36, md: 40, lg: 44, xl: 48, }, padding: { xs: 8, sm: 12, md: 16, lg: 20, xl: 24, }, borderRadius: { xs: 4, sm: 4, md: 8, lg: 16, xl: 24, full: 9999, }, },
const customTypography = { caption: { fontSize: 12, lineHeight: 16, fontWeight: '400' as const, }, small: { fontSize: 14, lineHeight: 20, fontWeight: '400' as const, }, body: { fontSize: 16, lineHeight: 24, fontWeight: '400' as const, }, subtitle: { fontSize: 18, lineHeight: 28, fontWeight: '500' as const, }, title: { fontSize: 20, lineHeight: 28, fontWeight: '600' as const, }, heading: { fontSize: 24, lineHeight: 32, fontWeight: '700' as const, },};
const customSpacing = { xs: 4, // 0.25rem sm: 8, // 0.5rem md: 16, // 1rem lg: 24, // 1.5rem xl: 32, // 2rem xxl: 48, // 3rem};
const customFontSizes = { xs: 12, // Small text sm: 14, // Body text md: 16, // Default size lg: 18, // Large text xl: 20, // Extra large xxl: 24, // Headings};
import { CustomThemeConfigFactory } from 'rnc-theme';
const customThemeConfig: CustomThemeConfigFactory = (isDark: boolean) => ({ colors: { primary: isDark ? '#FF6B6B' : '#4ECDC4', secondary: isDark ? '#FFE66D' : '#45B7D1', background: isDark ? '#1a1a1a' : '#f8f9fa', surface: isDark ? '#2d2d2d' : '#ffffff', text: isDark ? '#ffffff' : '#333333', textSecondary: isDark ? '#b0b0b0' : '#666666', border: isDark ? '#404040' : '#e0e0e0', error: '#FF5252', warning: '#FF9800', success: '#4CAF50', info: '#2196F3', muted: isDark ? '#666666' : '#999999', accent: isDark ? '#FF6B6B' : '#4ECDC4', destructive: '#FF5252', }, components: { height: { xs: 32, sm: 36, md: 40, lg: 44, xl: 48, }, padding: { xs: 8, sm: 12, md: 16, lg: 20, xl: 24, }, borderRadius: { xs: 4, sm: 4, md: 8, lg: 16, xl: 24, full: 9999, }, }, spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48, }, typography: { caption: { fontSize: 10, lineHeight: 14, fontWeight: '400' }, small: { fontSize: 12, lineHeight: 16, fontWeight: '400' }, body: { fontSize: 16, lineHeight: 24, fontWeight: '400' }, subtitle: { fontSize: 18, lineHeight: 26, fontWeight: '500' }, title: { fontSize: 20, lineHeight: 28, fontWeight: '600' }, heading: { fontSize: 24, lineHeight: 32, fontWeight: '700' }, }, fontSizes: { xs: 12, sm: 14, md: 16, lg: 18, xl: 20, xxl: 24, }, })
import { CustomThemeConfigFactory } from 'rnc-theme';
const createBrandTheme: CustomThemeConfigFactory = (isDark: boolean) => ({ colors: { primary: '#6366F1', secondary: '#8B5CF6', background: isDark ? '#0F172A' : '#F8FAFC', surface: isDark ? '#1E293B' : '#FFFFFF', text: isDark ? '#F1F5F9' : '#0F172A', textSecondary: isDark ? '#94A3B8' : '#64748B', border: isDark ? '#334155' : '#E2E8F0', error: '#EF4444', warning: '#F59E0B', success: '#10B981', info: '#3B82F6', muted: isDark ? '#64748B' : '#94A3B8', accent: '#EC4899', destructive: '#DC2626', }, // ...rest of the theme configuration});
import { RNCProvider } from 'rnc-theme';
const customTheme = { light: { colors: { primary: '#6366F1', // ... other light theme colors }, }, dark: { colors: { primary: '#818CF8', // ... other dark theme colors }, },};
export default function App() { return ( <RNCProvider customTheme={customTheme}> {/* Your app content */} </RNCProvider> );}
import { useTheme } from 'rnc-theme';
function ThemeCustomizer() { const { updateCustomTheme } = useTheme();
const applyBrandTheme = () => { updateCustomTheme( // ...props ); };
return ( <Button onPress={applyBrandTheme}> Apply Brand Theme </Button> );}
import { themeRegistry } from 'rnc-theme';
// Use in componentfunction ThemeSwitcher() { useEffect(()=>{ // Register custom theme themeRegistry.registerPreset('brand', createBrandTheme); },[])
return ( <Button> Switch to Brand Theme </Button> );}
import React, { useState, useCallback, useMemo, useEffect } from 'react';import { ScrollView, Alert, StatusBar, View, StyleSheet } from 'react-native';import { useTheme, useThemedStyles, Theme, CardHeader, CardContent, Card, Typography, Switcher, ButtonText, Button, themeRegistry, CustomThemeConfigFactory,} from 'rnc-theme';
type ThemePreset = | 'default' | 'material' | 'neon'
const ThemeScreen: React.FC = () => { const { theme, themeMode, setThemeMode, isDark, updateCustomTheme, resetTheme, } = useTheme(); const styles = useThemedStyles(createStyles); const [selectedPreset, setSelectedPreset] = useState<ThemePreset>('default'); const [appliedTheme, setAppliedTheme] = useState<ThemePreset>('default'); // Tambahkan state baru untuk preview const [previewTheme, setPreviewTheme] = useState<ThemePreset | null>(null); const [isDarkModeDisabled, setIsDarkModeDisabled] = useState(false);
useEffect(() => { // Register semua theme presets ke registry themeRegistry.registerPreset('material', materialThemeConfig); themeRegistry.registerPreset('neon', neonThemeConfig); }, []);
// Dynamic theme creators that respond to theme mode changes const createDynamicTheme = useCallback( (themeConfig: (isDark: boolean) => Partial<Theme>) => { // Generate both light and dark variants const lightTheme = themeConfig(false); const darkTheme = themeConfig(true);
// Update with both variants updateCustomTheme( isDark ? darkTheme : lightTheme, undefined, themeConfig ); }, [updateCustomTheme, isDark] );
// Improved toggle theme yang memperbarui tema aktif const toggleTheme = useCallback(() => { const newMode = isDark ? 'light' : 'dark'; setThemeMode(newMode);
// Tidak perlu setTimeout lagi karena ThemeContext akan otomatis // menggunakan tema yang sesuai dari storage berdasarkan mode baru }, [isDark, setThemeMode]);
// Define custom theme configurations
const materialThemeConfig: CustomThemeConfigFactory = useMemo( () => (isDark: boolean) => ({ colors: { primary: '#6200EE', secondary: '#03DAC6', background: isDark ? '#121212' : '#FFFFFF', surface: isDark ? '#1E1E1E' : '#FFFFFF', text: isDark ? '#FFFFFF' : '#000000', textSecondary: isDark ? '#B3B3B3' : '#757575', border: isDark ? '#2C2C2C' : '#E0E0E0', error: '#B00020', warning: '#FF6F00', success: '#00C853', info: '#2962FF', muted: isDark ? '#666666' : '#999999', accent: '#6200EE', destructive: '#B00020', }, components: { height: { xs: 32, sm: 36, md: 40, lg: 44, xl: 48, }, padding: { xs: 8, sm: 12, md: 16, lg: 20, xl: 24, }, borderRadius: { xs: 4, sm: 4, md: 4, lg: 8, xl: 12, full: 9999, }, }, spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48, }, typography: { caption: { fontSize: 10, lineHeight: 14, fontWeight: '400' }, small: { fontSize: 12, lineHeight: 16, fontWeight: '400' }, body: { fontSize: 16, lineHeight: 24, fontWeight: '400' }, subtitle: { fontSize: 18, lineHeight: 26, fontWeight: '500' }, title: { fontSize: 20, lineHeight: 28, fontWeight: '600' }, heading: { fontSize: 24, lineHeight: 32, fontWeight: '700' }, }, fontSizes: { xs: 12, sm: 14, md: 16, lg: 18, xl: 20, xxl: 24, }, }), [] );
const neonThemeConfig: CustomThemeConfigFactory = useMemo( () => (isDark: boolean) => ({ colors: { primary: '#00FFFF', secondary: '#FF00FF', background: isDark ? '#0a0a0a' : '#f0f0f0', surface: isDark ? '#1a1a1a' : '#ffffff', text: isDark ? '#00FFFF' : '#333333', textSecondary: isDark ? '#FF00FF' : '#666666', border: isDark ? '#00FFFF' : '#e0e0e0', error: '#FF0040', warning: '#FFFF00', success: '#00FF40', info: '#4080FF', muted: isDark ? '#666666' : '#999999', accent: '#FF00FF', destructive: '#FF0040', }, components: { height: { xs: 32, sm: 36, md: 40, lg: 44, xl: 48, }, padding: { xs: 8, sm: 12, md: 16, lg: 20, xl: 24, }, borderRadius: { xs: 2, sm: 2, md: 4, lg: 8, xl: 16, full: 9999, }, }, spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48, }, typography: { caption: { fontSize: 10, lineHeight: 14, fontWeight: '400' }, small: { fontSize: 12, lineHeight: 16, fontWeight: '400' }, body: { fontSize: 16, lineHeight: 24, fontWeight: '400' }, subtitle: { fontSize: 18, lineHeight: 26, fontWeight: '500' }, title: { fontSize: 20, lineHeight: 28, fontWeight: '600' }, heading: { fontSize: 24, lineHeight: 32, fontWeight: '700' }, }, fontSizes: { xs: 12, sm: 14, md: 16, lg: 18, xl: 20, xxl: 24, }, }), [] );
// Helper function untuk mendapatkan theme config berdasarkan preset const getThemeConfig = useCallback( (preset: ThemePreset) => { switch (preset) { case 'material': return materialThemeConfig; case 'neon': return neonThemeConfig; default: return null; } }, [ materialThemeConfig, neonThemeConfig, ] );
// Fungsi preview tema (tidak mengubah appliedTheme) const previewThemePreset = useCallback( (preset: ThemePreset) => { setSelectedPreset(preset); setPreviewTheme(preset); setIsDarkModeDisabled(preset !== 'default'); // Disable dark mode toggle saat preview
if (preset === 'default') { resetTheme(); setPreviewTheme(null); setIsDarkModeDisabled(false); } else { const themeConfig = getThemeConfig(preset); if (themeConfig) { createDynamicTheme(themeConfig); } } }, [createDynamicTheme, resetTheme, getThemeConfig] );
// Fungsi apply tema (mengubah appliedTheme) const applyThemePreset = useCallback( (preset: ThemePreset) => { setAppliedTheme(preset); setPreviewTheme(null); setIsDarkModeDisabled(false);
if (preset === 'default') { resetTheme(); } else { const themeConfig = getThemeConfig(preset); if (themeConfig) { // Generate both light and dark variants when applying const lightTheme = themeConfig(false); const darkTheme = themeConfig(true);
// Apply current theme variant and save the config for future mode switches updateCustomTheme( isDark ? darkTheme : lightTheme, preset, themeConfig ); } } }, [isDark, resetTheme, getThemeConfig, updateCustomTheme] );
// Fungsi cancel preview const cancelPreview = useCallback(() => { if (previewTheme && previewTheme !== appliedTheme) { // Kembali ke tema yang sedang diterapkan previewThemePreset(appliedTheme); setSelectedPreset(appliedTheme); } }, [previewTheme, appliedTheme, previewThemePreset]);
const showAlert = useCallback( (type: 'success' | 'error' | 'warning' | 'info') => { const messages = { success: 'Operasi berhasil!', error: 'Terjadi kesalahan!', warning: 'Peringatan: Periksa input Anda!', info: 'Informasi: Tema telah diperbarui!', }; Alert.alert('Notifikasi', messages[type]); }, [] );
const applySelectedTheme = useCallback(() => { applyThemePreset(selectedPreset); showAlert('success'); }, [selectedPreset, applyThemePreset, showAlert]);
return ( <ScrollView style={styles.container}> <StatusBar barStyle={isDark ? 'light-content' : 'dark-content'} backgroundColor={theme.colors.background} />
<Typography variant="heading" align="center" style={styles.title}> 🎨 Theme System Demo </Typography> <Typography variant="subtitle" align="center" style={styles.subtitle}> Mode saat ini: {themeMode} | Tema aktif: {appliedTheme} {previewTheme && previewTheme !== appliedTheme && ( <Typography variant="small" style={{ color: theme.colors.warning }}> {' '} | Preview: {previewTheme} </Typography> )} </Typography>
{/* Theme Controls */} <Card style={styles.card}> <CardHeader title="🎛️ Kontrol Tema" /> <CardContent> <View style={styles.row}> <Typography variant="body" style={{ opacity: isDarkModeDisabled ? 0.5 : 1, }} > Mode Gelap {isDarkModeDisabled && '(Dinonaktifkan saat preview)'} </Typography> <Switcher value={isDark} onValueChange={toggleTheme} disabled={isDarkModeDisabled} /> </View>
{previewTheme && previewTheme !== appliedTheme ? ( <View> <Button variant="primary" onPress={applySelectedTheme} style={styles.button} > <ButtonText> ✅ Terapkan Tema{' '} {selectedPreset.charAt(0).toUpperCase() + selectedPreset.slice(1)} </ButtonText> </Button>
<Button variant="outline" onPress={cancelPreview} style={styles.button} > <ButtonText>❌ Batal Preview</ButtonText> </Button> </View> ) : ( <Button variant="primary" onPress={applySelectedTheme} style={styles.button} > <ButtonText> {selectedPreset === 'default' ? 'Reset ke Tema Default' : `Terapkan Tema ${ selectedPreset.charAt(0).toUpperCase() + selectedPreset.slice(1) }`} </ButtonText> </Button> )}
<Button variant="outline" onPress={() => applyThemePreset('default')} style={styles.button} > <ButtonText>Reset Tema</ButtonText> </Button> </CardContent> </Card>
{/* Theme Presets */} <Card style={styles.card}> <CardHeader title="🎭 Preset Tema" /> <CardContent> <View style={styles.presetGrid}> {[ { key: 'default', label: 'Default' }, { key: 'material', label: 'Material' }, { key: 'neon', label: 'Neon' }, ].map(({ key, label }) => ( <Button key={key} variant={ selectedPreset === key ? 'primary' : appliedTheme === key ? 'success' : 'outline' } size="sm" onPress={() => previewThemePreset(key as ThemePreset)} style={styles.presetButton} > <ButtonText> {label} {appliedTheme === key && selectedPreset !== key && ' ✓'} {selectedPreset === key && previewTheme !== appliedTheme && ' 👁️'} </ButtonText> </Button> ))} </View> </CardContent> </Card>
{/* ... rest of existing code ... */} </ScrollView> );};
const createStyles = (theme: Theme) => StyleSheet.create({ container: { flex: 1, backgroundColor: theme.colors.background, padding: theme.spacing.md, }, title: { marginBottom: theme.spacing.sm, }, subtitle: { marginBottom: theme.spacing.lg, color: theme.colors.textSecondary, }, card: { marginBottom: theme.spacing.lg, }, row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: theme.spacing.md, }, button: { marginBottom: theme.spacing.sm, }, presetGrid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between', }, presetButton: { width: '32%', marginBottom: theme.spacing.sm, }, colorsGrid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between', }, colorItem: { alignItems: 'center', marginBottom: theme.spacing.md, width: '30%', }, colorBox: { width: 50, height: 50, borderRadius: theme.components.borderRadius.md, marginBottom: theme.spacing.xs, borderWidth: 1, borderColor: theme.colors.border, }, marginBottom: { marginBottom: theme.spacing.sm, }, buttonGrid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between', }, actionButton: { width: '48%', marginBottom: theme.spacing.sm, }, radiusGrid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between', }, radiusItem: { alignItems: 'center', marginBottom: theme.spacing.md, width: '48%', }, radiusBox: { width: 60, height: 60, backgroundColor: theme.colors.primary, marginBottom: theme.spacing.xs, }, spacingContainer: { alignItems: 'center', }, spacingBox: { marginBottom: theme.spacing.sm, },});
export default ThemeScreen;
import { Platform } from 'react-native';
const platformAwareTheme = { colors: { primary: '#6366F1', // ... other colors }, components: { borderRadius: { xs: Platform.OS === 'ios' ? 6 : 4, sm: Platform.OS === 'ios' ? 8 : 6, md: Platform.OS === 'ios' ? 10 : 8, lg: Platform.OS === 'ios' ? 12 : 10, xl: Platform.OS === 'ios' ? 16 : 12, full: 9999, }, height: { xs: Platform.OS === 'ios' ? 34 : 32, sm: Platform.OS === 'ios' ? 38 : 36, md: Platform.OS === 'ios' ? 42 : 40, lg: Platform.OS === 'ios' ? 46 : 44, xl: Platform.OS === 'ios' ? 50 : 48, }, },};
Color Consistency
Use a consistent color palette with proper contrast ratios for accessibility.
Spacing Scale
Maintain a consistent spacing scale (4px, 8px, 16px, 24px, 32px, 48px).
Typography Hierarchy
Establish clear typography hierarchy with appropriate font sizes and weights.
Component Consistency
Keep component sizing consistent across your theme (heights, padding, border radius).
// ✅ Use semantic color namescolors: { primary: '#6366F1', error: '#EF4444', success: '#10B981',}
// ✅ Maintain consistent spacing scalespacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48,}
// ✅ Use relative units for typographytypography: { body: { fontSize: 16, lineHeight: 24, // 1.5x font size },}
// ❌ Don't use arbitrary color namescolors: { myFavoriteBlue: '#6366F1', thatNiceRed: '#EF4444',}
// ❌ Don't use inconsistent spacingspacing: { xs: 3, sm: 7, md: 15, lg: 23,}
// ❌ Don't ignore accessibilitycolors: { text: '#CCCCCC', // Too light background: '#FFFFFF', // Poor contrast}
Need more details?
Check out our comprehensive API reference documentation for complete details on theme configuration options, types.