Skip to content

Custom Theme

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',
}
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 component
function 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 names
colors: {
primary: '#6366F1',
error: '#EF4444',
success: '#10B981',
}
// ✅ Maintain consistent spacing scale
spacing: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
xxl: 48,
}
// ✅ Use relative units for typography
typography: {
body: {
fontSize: 16,
lineHeight: 24, // 1.5x font size
},
}

Need more details?

Check out our comprehensive API reference documentation for complete details on theme configuration options, types.