
React Native Design
- 12.4k installs
- 38.3k repo stars
- Updated July 22, 2026
- wshobson/agents
react-native-design is a skill providing React Native styling, navigation, and animation patterns for cross-platform apps.
About
react-native-design provides patterns for React Native cross-platform mobile development. It covers styling (StyleSheet, platform-specific), navigation (React Navigation stack/drawer/bottom-tab), and animations (Reanimated 3 shared values and worklets). It includes responsive layout patterns, gesture handling, and performance best practices. Developers use it to build high-quality native mobile experiences.
- React Native styling, React Navigation 6+, Reanimated 3 animations
- Cross-platform responsive layouts with iOS/Android platform-specific handling
- Performance optimization (memoization, StyleSheet, FlatList patterns)
React Native Design by the numbers
- 12,369 all-time installs (skills.sh)
- +287 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #15 of 1,048 Mobile Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
react-native-design capabilities & compatibility
- Capabilities
- ui design · performance optimization
- Use cases
- ui design
- Platforms
- macOS · Windows · Linux
- Runs
- Hosted SaaS
What react-native-design says it does
Master React Native styling patterns, React Navigation, and Reanimated 3 to build performant, cross-platform mobile applications
npx skills add https://github.com/wshobson/agents --skill react-native-designAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 12.4k |
|---|---|
| repo stars | ★ 38.3k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 22, 2026 |
| Repository | wshobson/agents ↗ |
What it does
react-native-design provides patterns for React Native cross-platform mobile development. It covers styling (StyleSheet, platform-specific), navigation (React Navigation stack/drawer/bottom-tab), and
Who is it for?
React developers building cross-platform mobile apps with React Native and Reanimated
Skip if: Web-only or non-React projects
When should I use this skill?
Building React Native mobile apps, implementing navigation, creating animations, or optimizing performance
What you get
Cross-platform mobile app with smooth 60fps animations, responsive layouts, and iOS/Android-specific polish
- stylesheet modules
- themed component styles
- flexbox layout files
Files
React Native Design
Master React Native styling patterns, React Navigation, and Reanimated 3 to build performant, cross-platform mobile applications with native-quality user experiences.
When to Use This Skill
- Building cross-platform mobile apps with React Native
- Implementing navigation with React Navigation 6+
- Creating performant animations with Reanimated 3
- Styling components with StyleSheet and styled-components
- Building responsive layouts for different screen sizes
- Implementing platform-specific designs (iOS/Android)
- Creating gesture-driven interactions with Gesture Handler
- Optimizing React Native performance
Detailed section: Core Concepts
Originally a 6471-byte section in this SKILL.md. Moved to references/details.md to fit Codex's 8 KB skill body cap.
Quick Start Component
import React from 'react';
import {
View,
Text,
StyleSheet,
Pressable,
Image,
} from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
interface ItemCardProps {
title: string;
subtitle: string;
imageUrl: string;
onPress: () => void;
}
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
export function ItemCard({ title, subtitle, imageUrl, onPress }: ItemCardProps) {
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
<AnimatedPressable
style={[styles.card, animatedStyle]}
onPress={onPress}
onPressIn={() => { scale.value = withSpring(0.97); }}
onPressOut={() => { scale.value = withSpring(1); }}
>
<Image source={{ uri: imageUrl }} style={styles.image} />
<View style={styles.content}>
<Text style={styles.title} numberOfLines={1}>
{title}
</Text>
<Text style={styles.subtitle} numberOfLines={2}>
{subtitle}
</Text>
</View>
</AnimatedPressable>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: '#ffffff',
borderRadius: 16,
overflow: 'hidden',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 4,
},
image: {
width: '100%',
height: 160,
backgroundColor: '#f3f4f6',
},
content: {
padding: 16,
gap: 4,
},
title: {
fontSize: 18,
fontWeight: '600',
color: '#1f2937',
},
subtitle: {
fontSize: 14,
color: '#6b7280',
lineHeight: 20,
},
});Best Practices
1. Use TypeScript: Define navigation and prop types for type safety 2. Memoize Components: Use React.memo and useCallback to prevent unnecessary rerenders 3. Run Animations on UI Thread: Use Reanimated worklets for 60fps animations 4. Avoid Inline Styles: Use StyleSheet.create for performance 5. Handle Safe Areas: Use SafeAreaView or useSafeAreaInsets 6. Test on Real Devices: Simulator/emulator performance differs from real devices 7. Use FlatList for Lists: Never use ScrollView with map for long lists 8. Platform-Specific Code: Use Platform.select for iOS/Android differences
Common Issues
- Gesture Conflicts: Wrap gestures with
GestureDetectorand usesimultaneousHandlers - Navigation Type Errors: Define
ParamListtypes for all navigators - Animation Jank: Move animations to UI thread with
runOnUIworklets - Memory Leaks: Cancel animations and cleanup in useEffect
- Font Loading: Use
expo-fontorreact-native-assetfor custom fonts - Safe Area Issues: Test on notched devices (iPhone, Android with cutouts)
react-native-design — detailed sections
Core Concepts
1. StyleSheet and Styling
Basic StyleSheet:
import { StyleSheet, View, Text } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: '#ffffff',
},
title: {
fontSize: 24,
fontWeight: '600',
color: '#1a1a1a',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#666666',
lineHeight: 24,
},
});
function Card() {
return (
<View style={styles.container}>
<Text style={styles.title}>Title</Text>
<Text style={styles.subtitle}>Subtitle text</Text>
</View>
);
}Dynamic Styles:
interface CardProps {
variant: 'primary' | 'secondary';
disabled?: boolean;
}
function Card({ variant, disabled }: CardProps) {
return (
<View style={[
styles.card,
variant === 'primary' ? styles.primary : styles.secondary,
disabled && styles.disabled,
]}>
<Text style={styles.text}>Content</Text>
</View>
);
}
const styles = StyleSheet.create({
card: {
padding: 16,
borderRadius: 12,
},
primary: {
backgroundColor: '#6366f1',
},
secondary: {
backgroundColor: '#f3f4f6',
borderWidth: 1,
borderColor: '#e5e7eb',
},
disabled: {
opacity: 0.5,
},
text: {
fontSize: 16,
},
});2. Flexbox Layout
Row and Column Layouts:
const styles = StyleSheet.create({
// Vertical stack (column)
column: {
flexDirection: "column",
gap: 12,
},
// Horizontal stack (row)
row: {
flexDirection: "row",
alignItems: "center",
gap: 8,
},
// Space between items
spaceBetween: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
// Centered content
centered: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
// Fill remaining space
fill: {
flex: 1,
},
});3. React Navigation Setup
Stack Navigator:
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
type RootStackParamList = {
Home: undefined;
Detail: { itemId: string };
Settings: undefined;
};
const Stack = createNativeStackNavigator<RootStackParamList>();
function AppNavigator() {
return (
<NavigationContainer>
<Stack.Navigator
initialRouteName="Home"
screenOptions={{
headerStyle: { backgroundColor: '#6366f1' },
headerTintColor: '#ffffff',
headerTitleStyle: { fontWeight: '600' },
}}
>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: 'Home' }}
/>
<Stack.Screen
name="Detail"
component={DetailScreen}
options={({ route }) => ({
title: `Item ${route.params.itemId}`,
})}
/>
<Stack.Screen name="Settings" component={SettingsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}Tab Navigator:
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
type TabParamList = {
Home: undefined;
Search: undefined;
Profile: undefined;
};
const Tab = createBottomTabNavigator<TabParamList>();
function TabNavigator() {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
const icons: Record<string, keyof typeof Ionicons.glyphMap> = {
Home: focused ? 'home' : 'home-outline',
Search: focused ? 'search' : 'search-outline',
Profile: focused ? 'person' : 'person-outline',
};
return <Ionicons name={icons[route.name]} size={size} color={color} />;
},
tabBarActiveTintColor: '#6366f1',
tabBarInactiveTintColor: '#9ca3af',
})}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Search" component={SearchScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
}4. Reanimated 3 Basics
Animated Values:
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
} from 'react-native-reanimated';
function AnimatedBox() {
const scale = useSharedValue(1);
const opacity = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
opacity: opacity.value,
}));
const handlePress = () => {
scale.value = withSpring(1.2, {}, () => {
scale.value = withSpring(1);
});
};
return (
<Pressable onPress={handlePress}>
<Animated.View style={[styles.box, animatedStyle]} />
</Pressable>
);
}Gesture Handler Integration:
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
function DraggableCard() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const gesture = Gesture.Pan()
.onUpdate((event) => {
translateX.value = event.translationX;
translateY.value = event.translationY;
})
.onEnd(() => {
translateX.value = withSpring(0);
translateY.value = withSpring(0);
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
}));
return (
<GestureDetector gesture={gesture}>
<Animated.View style={[styles.card, animatedStyle]}>
<Text>Drag me!</Text>
</Animated.View>
</GestureDetector>
);
}5. Platform-Specific Styling
import { Platform, StyleSheet } from "react-native";
const styles = StyleSheet.create({
container: {
padding: 16,
...Platform.select({
ios: {
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
android: {
elevation: 4,
},
}),
},
text: {
fontFamily: Platform.OS === "ios" ? "SF Pro Text" : "Roboto",
fontSize: 16,
},
});
// Platform-specific components
import { Platform } from "react-native";
const StatusBarHeight = Platform.OS === "ios" ? 44 : 0;React Navigation Patterns
Setup and Configuration
Installation
# Core packages
npm install @react-navigation/native
npm install @react-navigation/native-stack
npm install @react-navigation/bottom-tabs
# Required peer dependencies
npm install react-native-screens react-native-safe-area-contextType-Safe Navigation Setup
// navigation/types.ts
import { NativeStackScreenProps } from "@react-navigation/native-stack";
import { BottomTabScreenProps } from "@react-navigation/bottom-tabs";
import {
CompositeScreenProps,
NavigatorScreenParams,
} from "@react-navigation/native";
// Define param lists for each navigator
export type RootStackParamList = {
Main: NavigatorScreenParams<MainTabParamList>;
Modal: { title: string };
Auth: NavigatorScreenParams<AuthStackParamList>;
};
export type MainTabParamList = {
Home: undefined;
Search: { query?: string };
Profile: { userId: string };
};
export type AuthStackParamList = {
Login: undefined;
Register: undefined;
ForgotPassword: { email?: string };
};
// Screen props helpers
export type RootStackScreenProps<T extends keyof RootStackParamList> =
NativeStackScreenProps<RootStackParamList, T>;
export type MainTabScreenProps<T extends keyof MainTabParamList> =
CompositeScreenProps<
BottomTabScreenProps<MainTabParamList, T>,
RootStackScreenProps<keyof RootStackParamList>
>;
// Global type declaration
declare global {
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}Navigation Hooks
// hooks/useAppNavigation.ts
import { useNavigation, useRoute, RouteProp } from "@react-navigation/native";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { RootStackParamList } from "./types";
export function useAppNavigation() {
return useNavigation<NativeStackNavigationProp<RootStackParamList>>();
}
export function useTypedRoute<T extends keyof RootStackParamList>() {
return useRoute<RouteProp<RootStackParamList, T>>();
}Stack Navigation
Basic Stack Navigator
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { RootStackParamList } from './types';
const Stack = createNativeStackNavigator<RootStackParamList>();
function RootNavigator() {
return (
<Stack.Navigator
initialRouteName="Main"
screenOptions={{
headerStyle: { backgroundColor: '#6366f1' },
headerTintColor: '#ffffff',
headerTitleStyle: { fontWeight: '600' },
headerBackTitleVisible: false,
animation: 'slide_from_right',
}}
>
<Stack.Screen
name="Main"
component={MainTabNavigator}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Modal"
component={ModalScreen}
options={{
presentation: 'modal',
animation: 'slide_from_bottom',
}}
/>
<Stack.Group screenOptions={{ presentation: 'fullScreenModal' }}>
<Stack.Screen
name="Auth"
component={AuthNavigator}
options={{ headerShown: false }}
/>
</Stack.Group>
</Stack.Navigator>
);
}Screen with Dynamic Options
function DetailScreen({ route, navigation }: DetailScreenProps) {
const { itemId } = route.params;
const [item, setItem] = useState<Item | null>(null);
useEffect(() => {
// Update header when data loads
if (item) {
navigation.setOptions({
title: item.title,
headerRight: () => (
<TouchableOpacity onPress={() => shareItem(item)}>
<Ionicons name="share-outline" size={24} color="#ffffff" />
</TouchableOpacity>
),
});
}
}, [item, navigation]);
// Prevent going back with unsaved changes
useEffect(() => {
const unsubscribe = navigation.addListener('beforeRemove', (e) => {
if (!hasUnsavedChanges) return;
e.preventDefault();
Alert.alert(
'Discard changes?',
'You have unsaved changes. Are you sure you want to leave?',
[
{ text: "Don't leave", style: 'cancel' },
{
text: 'Discard',
style: 'destructive',
onPress: () => navigation.dispatch(e.data.action),
},
]
);
});
return unsubscribe;
}, [navigation, hasUnsavedChanges]);
return <View>{/* Content */}</View>;
}Tab Navigation
Bottom Tab Navigator
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { MainTabParamList } from './types';
import { Ionicons } from '@expo/vector-icons';
const Tab = createBottomTabNavigator<MainTabParamList>();
function MainTabNavigator() {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
const icons: Record<keyof MainTabParamList, string> = {
Home: focused ? 'home' : 'home-outline',
Search: focused ? 'search' : 'search-outline',
Profile: focused ? 'person' : 'person-outline',
};
return (
<Ionicons
name={icons[route.name] as any}
size={size}
color={color}
/>
);
},
tabBarActiveTintColor: '#6366f1',
tabBarInactiveTintColor: '#9ca3af',
tabBarStyle: {
backgroundColor: '#ffffff',
borderTopWidth: 1,
borderTopColor: '#e5e7eb',
paddingBottom: 8,
paddingTop: 8,
height: 60,
},
tabBarLabelStyle: {
fontSize: 12,
fontWeight: '500',
},
headerStyle: { backgroundColor: '#ffffff' },
headerTitleStyle: { fontWeight: '600' },
})}
>
<Tab.Screen
name="Home"
component={HomeScreen}
options={{
tabBarLabel: 'Home',
tabBarBadge: 3,
}}
/>
<Tab.Screen
name="Search"
component={SearchScreen}
options={{ tabBarLabel: 'Search' }}
/>
<Tab.Screen
name="Profile"
component={ProfileScreen}
options={{ tabBarLabel: 'Profile' }}
/>
</Tab.Navigator>
);
}Custom Tab Bar
import { View, Pressable, StyleSheet } from 'react-native';
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
function CustomTabBar({ state, descriptors, navigation }: BottomTabBarProps) {
return (
<View style={styles.tabBar}>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key];
const label = options.tabBarLabel ?? route.name;
const isFocused = state.index === index;
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
navigation.navigate(route.name);
}
};
return (
<TabBarButton
key={route.key}
label={label as string}
isFocused={isFocused}
onPress={onPress}
icon={options.tabBarIcon}
/>
);
})}
</View>
);
}
function TabBarButton({ label, isFocused, onPress, icon }) {
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
<Pressable
onPress={onPress}
onPressIn={() => { scale.value = withSpring(0.9); }}
onPressOut={() => { scale.value = withSpring(1); }}
style={styles.tabButton}
>
<Animated.View style={animatedStyle}>
{icon?.({
focused: isFocused,
color: isFocused ? '#6366f1' : '#9ca3af',
size: 24,
})}
<Text
style={[
styles.tabLabel,
{ color: isFocused ? '#6366f1' : '#9ca3af' },
]}
>
{label}
</Text>
</Animated.View>
</Pressable>
);
}
const styles = StyleSheet.create({
tabBar: {
flexDirection: 'row',
backgroundColor: '#ffffff',
paddingBottom: 20,
paddingTop: 12,
borderTopWidth: 1,
borderTopColor: '#e5e7eb',
},
tabButton: {
flex: 1,
alignItems: 'center',
},
tabLabel: {
fontSize: 12,
marginTop: 4,
fontWeight: '500',
},
});
// Usage
<Tab.Navigator tabBar={(props) => <CustomTabBar {...props} />}>
{/* screens */}
</Tab.Navigator>Drawer Navigation
import {
createDrawerNavigator,
DrawerContentScrollView,
DrawerItemList,
DrawerContentComponentProps,
} from '@react-navigation/drawer';
const Drawer = createDrawerNavigator();
function CustomDrawerContent(props: DrawerContentComponentProps) {
return (
<DrawerContentScrollView {...props}>
<View style={styles.drawerHeader}>
<Image source={{ uri: user.avatar }} style={styles.avatar} />
<Text style={styles.userName}>{user.name}</Text>
<Text style={styles.userEmail}>{user.email}</Text>
</View>
<DrawerItemList {...props} />
<View style={styles.drawerFooter}>
<TouchableOpacity
onPress={handleLogout}
style={styles.logoutButton}
>
<Ionicons name="log-out-outline" size={24} color="#ef4444" />
<Text style={styles.logoutText}>Log Out</Text>
</TouchableOpacity>
</View>
</DrawerContentScrollView>
);
}
function DrawerNavigator() {
return (
<Drawer.Navigator
drawerContent={(props) => <CustomDrawerContent {...props} />}
screenOptions={{
drawerActiveBackgroundColor: '#ede9fe',
drawerActiveTintColor: '#6366f1',
drawerInactiveTintColor: '#4b5563',
drawerLabelStyle: { marginLeft: -20, fontSize: 15, fontWeight: '500' },
drawerStyle: { width: 280 },
}}
>
<Drawer.Screen
name="Home"
component={HomeScreen}
options={{
drawerIcon: ({ color }) => (
<Ionicons name="home-outline" size={22} color={color} />
),
}}
/>
<Drawer.Screen
name="Settings"
component={SettingsScreen}
options={{
drawerIcon: ({ color }) => (
<Ionicons name="settings-outline" size={22} color={color} />
),
}}
/>
</Drawer.Navigator>
);
}Deep Linking
Configuration
// navigation/linking.ts
import { LinkingOptions } from '@react-navigation/native';
import { RootStackParamList } from './types';
export const linking: LinkingOptions<RootStackParamList> = {
prefixes: ['myapp://', 'https://myapp.com'],
config: {
screens: {
Main: {
screens: {
Home: 'home',
Search: 'search',
Profile: 'profile/:userId',
},
},
Modal: 'modal/:title',
Auth: {
screens: {
Login: 'login',
Register: 'register',
ForgotPassword: 'forgot-password',
},
},
},
},
// Custom URL parsing
getStateFromPath: (path, config) => {
// Handle custom URL patterns
return getStateFromPath(path, config);
},
};
// App.tsx
function App() {
return (
<NavigationContainer linking={linking} fallback={<LoadingScreen />}>
<RootNavigator />
</NavigationContainer>
);
}Handling Deep Links
import { useEffect } from "react";
import { Linking } from "react-native";
import { useNavigation } from "@react-navigation/native";
function useDeepLinkHandler() {
const navigation = useNavigation();
useEffect(() => {
// Handle initial URL
const handleInitialUrl = async () => {
const url = await Linking.getInitialURL();
if (url) {
handleDeepLink(url);
}
};
// Handle URL changes
const subscription = Linking.addEventListener("url", ({ url }) => {
handleDeepLink(url);
});
handleInitialUrl();
return () => subscription.remove();
}, []);
const handleDeepLink = (url: string) => {
// Parse URL and navigate
const route = parseUrl(url);
if (route) {
navigation.navigate(route.name, route.params);
}
};
}Navigation State Management
Auth Flow
import { createContext, useContext, useState, useEffect } from 'react';
interface AuthContextType {
user: User | null;
isLoading: boolean;
signIn: (email: string, password: string) => Promise<void>;
signOut: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType>(null!);
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Check for existing session
checkAuthState();
}, []);
const checkAuthState = async () => {
try {
const token = await AsyncStorage.getItem('token');
if (token) {
const user = await fetchUser(token);
setUser(user);
}
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
const signIn = async (email: string, password: string) => {
const { user, token } = await loginApi(email, password);
await AsyncStorage.setItem('token', token);
setUser(user);
};
const signOut = async () => {
await AsyncStorage.removeItem('token');
setUser(null);
};
return (
<AuthContext.Provider value={{ user, isLoading, signIn, signOut }}>
{children}
</AuthContext.Provider>
);
}
function RootNavigator() {
const { user, isLoading } = useAuth();
if (isLoading) {
return <SplashScreen />;
}
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
{user ? (
<Stack.Screen name="Main" component={MainNavigator} />
) : (
<Stack.Screen name="Auth" component={AuthNavigator} />
)}
</Stack.Navigator>
);
}Navigation State Persistence
import AsyncStorage from '@react-native-async-storage/async-storage';
import { NavigationContainer, NavigationState } from '@react-navigation/native';
const PERSISTENCE_KEY = 'NAVIGATION_STATE';
function App() {
const [isReady, setIsReady] = useState(false);
const [initialState, setInitialState] = useState<NavigationState | undefined>();
useEffect(() => {
const restoreState = async () => {
try {
const savedState = await AsyncStorage.getItem(PERSISTENCE_KEY);
if (savedState) {
setInitialState(JSON.parse(savedState));
}
} catch (e) {
console.error('Failed to restore navigation state:', e);
} finally {
setIsReady(true);
}
};
if (!isReady) {
restoreState();
}
}, [isReady]);
if (!isReady) {
return <SplashScreen />;
}
return (
<NavigationContainer
initialState={initialState}
onStateChange={(state) => {
AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state));
}}
>
<RootNavigator />
</NavigationContainer>
);
}Screen Transitions
Custom Animations
import { TransitionPresets } from '@react-navigation/native-stack';
<Stack.Navigator
screenOptions={{
...TransitionPresets.SlideFromRightIOS,
gestureEnabled: true,
gestureDirection: 'horizontal',
}}
>
{/* Standard slide transition */}
<Stack.Screen name="List" component={ListScreen} />
{/* Modal with custom animation */}
<Stack.Screen
name="Modal"
component={ModalScreen}
options={{
presentation: 'transparentModal',
animation: 'fade',
cardOverlayEnabled: true,
}}
/>
{/* Full screen modal */}
<Stack.Screen
name="FullScreenModal"
component={FullScreenModalScreen}
options={{
presentation: 'fullScreenModal',
animation: 'slide_from_bottom',
}}
/>
</Stack.Navigator>Shared Element Transitions
import { SharedElement } from 'react-navigation-shared-element';
import { createSharedElementStackNavigator } from 'react-navigation-shared-element';
const Stack = createSharedElementStackNavigator();
function ListScreen({ navigation }) {
return (
<FlatList
data={items}
renderItem={({ item }) => (
<Pressable onPress={() => navigation.navigate('Detail', { item })}>
<SharedElement id={`item.${item.id}.photo`}>
<Image source={{ uri: item.imageUrl }} style={styles.image} />
</SharedElement>
<SharedElement id={`item.${item.id}.title`}>
<Text style={styles.title}>{item.title}</Text>
</SharedElement>
</Pressable>
)}
/>
);
}
function DetailScreen({ route }) {
const { item } = route.params;
return (
<View>
<SharedElement id={`item.${item.id}.photo`}>
<Image source={{ uri: item.imageUrl }} style={styles.heroImage} />
</SharedElement>
<SharedElement id={`item.${item.id}.title`}>
<Text style={styles.title}>{item.title}</Text>
</SharedElement>
</View>
);
}
// Navigator configuration
<Stack.Navigator>
<Stack.Screen name="List" component={ListScreen} />
<Stack.Screen
name="Detail"
component={DetailScreen}
sharedElements={(route) => {
const { item } = route.params;
return [
{ id: `item.${item.id}.photo`, animation: 'move' },
{ id: `item.${item.id}.title`, animation: 'fade' },
];
}}
/>
</Stack.Navigator>Header Customization
Custom Header Component
import { getHeaderTitle } from '@react-navigation/elements';
import { NativeStackHeaderProps } from '@react-navigation/native-stack';
function CustomHeader({ navigation, route, options, back }: NativeStackHeaderProps) {
const title = getHeaderTitle(options, route.name);
return (
<View style={styles.header}>
{back && (
<TouchableOpacity
onPress={navigation.goBack}
style={styles.backButton}
>
<Ionicons name="arrow-back" size={24} color="#1f2937" />
</TouchableOpacity>
)}
<Text style={styles.title}>{title}</Text>
{options.headerRight && (
<View style={styles.rightActions}>
{options.headerRight({ canGoBack: !!back })}
</View>
)}
</View>
);
}
// Usage
<Stack.Navigator
screenOptions={{
header: (props) => <CustomHeader {...props} />,
}}
>
{/* screens */}
</Stack.Navigator>Collapsible Header
import Animated, {
useSharedValue,
useAnimatedScrollHandler,
useAnimatedStyle,
interpolate,
Extrapolation,
} from 'react-native-reanimated';
const HEADER_HEIGHT = 200;
const COLLAPSED_HEIGHT = 60;
function CollapsibleHeaderScreen() {
const scrollY = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollY.value = event.contentOffset.y;
},
});
const headerStyle = useAnimatedStyle(() => {
const height = interpolate(
scrollY.value,
[0, HEADER_HEIGHT - COLLAPSED_HEIGHT],
[HEADER_HEIGHT, COLLAPSED_HEIGHT],
Extrapolation.CLAMP
);
return { height };
});
const titleStyle = useAnimatedStyle(() => {
const fontSize = interpolate(
scrollY.value,
[0, HEADER_HEIGHT - COLLAPSED_HEIGHT],
[32, 18],
Extrapolation.CLAMP
);
return { fontSize };
});
return (
<View style={styles.container}>
<Animated.View style={[styles.header, headerStyle]}>
<Animated.Text style={[styles.title, titleStyle]}>
Title
</Animated.Text>
</Animated.View>
<Animated.ScrollView
onScroll={scrollHandler}
scrollEventThrottle={16}
contentContainerStyle={{ paddingTop: HEADER_HEIGHT }}
>
{/* Content */}
</Animated.ScrollView>
</View>
);
}React Native Reanimated 3 Patterns
Core Concepts
Shared Values and Animated Styles
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
withDelay,
withSequence,
withRepeat,
Easing,
} from 'react-native-reanimated';
function BasicAnimations() {
// Shared value - can be modified from JS or UI thread
const opacity = useSharedValue(0);
const scale = useSharedValue(1);
const rotation = useSharedValue(0);
// Animated style - runs on UI thread
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
transform: [
{ scale: scale.value },
{ rotate: `${rotation.value}deg` },
],
}));
const animate = () => {
// Spring animation
scale.value = withSpring(1.2, {
damping: 10,
stiffness: 100,
});
// Timing animation with easing
opacity.value = withTiming(1, {
duration: 500,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
});
// Sequence of animations
rotation.value = withSequence(
withTiming(15, { duration: 100 }),
withTiming(-15, { duration: 100 }),
withTiming(0, { duration: 100 })
);
};
return (
<Animated.View style={[styles.box, animatedStyle]} />
);
}Animation Callbacks
import { runOnJS, runOnUI } from 'react-native-reanimated';
function AnimationWithCallbacks() {
const translateX = useSharedValue(0);
const [status, setStatus] = useState('idle');
const updateStatus = (newStatus: string) => {
setStatus(newStatus);
};
const animate = () => {
translateX.value = withTiming(
200,
{ duration: 1000 },
(finished) => {
'worklet';
if (finished) {
// Call JS function from worklet
runOnJS(updateStatus)('completed');
}
}
);
};
return (
<Animated.View
style={[
styles.box,
useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
})),
]}
/>
);
}Gesture Handler Integration
Pan Gesture
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
clamp,
} from 'react-native-reanimated';
function DraggableBox() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const context = useSharedValue({ x: 0, y: 0 });
const panGesture = Gesture.Pan()
.onStart(() => {
context.value = { x: translateX.value, y: translateY.value };
})
.onUpdate((event) => {
translateX.value = event.translationX + context.value.x;
translateY.value = event.translationY + context.value.y;
})
.onEnd((event) => {
// Apply velocity decay
translateX.value = withSpring(
clamp(translateX.value + event.velocityX / 10, -100, 100)
);
translateY.value = withSpring(
clamp(translateY.value + event.velocityY / 10, -100, 100)
);
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
}));
return (
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.box, animatedStyle]} />
</GestureDetector>
);
}Pinch and Rotate Gestures
function ZoomableImage() {
const scale = useSharedValue(1);
const rotation = useSharedValue(0);
const savedScale = useSharedValue(1);
const savedRotation = useSharedValue(0);
const pinchGesture = Gesture.Pinch()
.onUpdate((event) => {
scale.value = savedScale.value * event.scale;
})
.onEnd(() => {
savedScale.value = scale.value;
// Snap back if too small
if (scale.value < 1) {
scale.value = withSpring(1);
savedScale.value = 1;
}
});
const rotateGesture = Gesture.Rotation()
.onUpdate((event) => {
rotation.value = savedRotation.value + event.rotation;
})
.onEnd(() => {
savedRotation.value = rotation.value;
});
const composed = Gesture.Simultaneous(pinchGesture, rotateGesture);
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ scale: scale.value },
{ rotate: `${rotation.value}rad` },
],
}));
return (
<GestureDetector gesture={composed}>
<Animated.Image
source={{ uri: imageUrl }}
style={[styles.image, animatedStyle]}
/>
</GestureDetector>
);
}Tap Gesture with Feedback
function TappableCard({ onPress, children }: TappableCardProps) {
const scale = useSharedValue(1);
const opacity = useSharedValue(1);
const tapGesture = Gesture.Tap()
.onBegin(() => {
scale.value = withSpring(0.97);
opacity.value = withTiming(0.8, { duration: 100 });
})
.onFinalize(() => {
scale.value = withSpring(1);
opacity.value = withTiming(1, { duration: 100 });
})
.onEnd(() => {
runOnJS(onPress)();
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
opacity: opacity.value,
}));
return (
<GestureDetector gesture={tapGesture}>
<Animated.View style={[styles.card, animatedStyle]}>
{children}
</Animated.View>
</GestureDetector>
);
}Common Animation Patterns
Fade In/Out
function FadeInView({ visible, children }: FadeInViewProps) {
const opacity = useSharedValue(0);
useEffect(() => {
opacity.value = withTiming(visible ? 1 : 0, { duration: 300 });
}, [visible]);
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
display: opacity.value === 0 ? 'none' : 'flex',
}));
return (
<Animated.View style={animatedStyle}>
{children}
</Animated.View>
);
}Slide In/Out
function SlideInView({ visible, direction = 'right', children }) {
const translateX = useSharedValue(direction === 'right' ? 100 : -100);
const opacity = useSharedValue(0);
useEffect(() => {
if (visible) {
translateX.value = withSpring(0);
opacity.value = withTiming(1);
} else {
translateX.value = withSpring(direction === 'right' ? 100 : -100);
opacity.value = withTiming(0);
}
}, [visible, direction]);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
opacity: opacity.value,
}));
return (
<Animated.View style={animatedStyle}>
{children}
</Animated.View>
);
}Staggered List Animation
function StaggeredList({ items }: { items: Item[] }) {
return (
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={({ item, index }) => (
<StaggeredItem item={item} index={index} />
)}
/>
);
}
function StaggeredItem({ item, index }: { item: Item; index: number }) {
const translateY = useSharedValue(50);
const opacity = useSharedValue(0);
useEffect(() => {
translateY.value = withDelay(
index * 100,
withSpring(0, { damping: 15 })
);
opacity.value = withDelay(
index * 100,
withTiming(1, { duration: 300 })
);
}, [index]);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateY: translateY.value }],
opacity: opacity.value,
}));
return (
<Animated.View style={[styles.listItem, animatedStyle]}>
<Text>{item.title}</Text>
</Animated.View>
);
}Pulse Animation
function PulseView({ children }: { children: React.ReactNode }) {
const scale = useSharedValue(1);
useEffect(() => {
scale.value = withRepeat(
withSequence(
withTiming(1.05, { duration: 500 }),
withTiming(1, { duration: 500 })
),
-1, // infinite
false // no reverse
);
return () => {
cancelAnimation(scale);
};
}, []);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
<Animated.View style={animatedStyle}>
{children}
</Animated.View>
);
}Shake Animation
function ShakeView({ trigger, children }) {
const translateX = useSharedValue(0);
useEffect(() => {
if (trigger) {
translateX.value = withSequence(
withTiming(-10, { duration: 50 }),
withTiming(10, { duration: 50 }),
withTiming(-10, { duration: 50 }),
withTiming(10, { duration: 50 }),
withTiming(0, { duration: 50 })
);
}
}, [trigger]);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
return (
<Animated.View style={animatedStyle}>
{children}
</Animated.View>
);
}Advanced Patterns
Interpolation
import { interpolate, Extrapolation } from 'react-native-reanimated';
function ParallaxHeader() {
const scrollY = useSharedValue(0);
const headerStyle = useAnimatedStyle(() => {
const height = interpolate(
scrollY.value,
[0, 200],
[300, 100],
Extrapolation.CLAMP
);
const opacity = interpolate(
scrollY.value,
[0, 150, 200],
[1, 0.5, 0],
Extrapolation.CLAMP
);
const translateY = interpolate(
scrollY.value,
[0, 200],
[0, -50],
Extrapolation.CLAMP
);
return {
height,
opacity,
transform: [{ translateY }],
};
});
return (
<View style={styles.container}>
<Animated.View style={[styles.header, headerStyle]}>
<Text style={styles.headerTitle}>Header</Text>
</Animated.View>
<Animated.ScrollView
onScroll={useAnimatedScrollHandler({
onScroll: (event) => {
scrollY.value = event.contentOffset.y;
},
})}
scrollEventThrottle={16}
>
{/* Content */}
</Animated.ScrollView>
</View>
);
}Color Interpolation
import { interpolateColor } from 'react-native-reanimated';
function ColorTransition() {
const progress = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => {
const backgroundColor = interpolateColor(
progress.value,
[0, 0.5, 1],
['#6366f1', '#8b5cf6', '#ec4899']
);
return { backgroundColor };
});
const toggleColor = () => {
progress.value = withTiming(progress.value === 0 ? 1 : 0, {
duration: 1000,
});
};
return (
<Pressable onPress={toggleColor}>
<Animated.View style={[styles.box, animatedStyle]} />
</Pressable>
);
}Derived Values
import { useDerivedValue } from 'react-native-reanimated';
function DerivedValueExample() {
const x = useSharedValue(0);
const y = useSharedValue(0);
// Derived value computed from other shared values
const distance = useDerivedValue(() => {
return Math.sqrt(x.value ** 2 + y.value ** 2);
});
const angle = useDerivedValue(() => {
return Math.atan2(y.value, x.value) * (180 / Math.PI);
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: x.value },
{ translateY: y.value },
{ rotate: `${angle.value}deg` },
],
opacity: interpolate(distance.value, [0, 200], [1, 0.5]),
}));
return (
<Animated.View style={[styles.box, animatedStyle]} />
);
}Layout Animations
import Animated, {
Layout,
FadeIn,
FadeOut,
SlideInLeft,
SlideOutRight,
ZoomIn,
ZoomOut,
} from 'react-native-reanimated';
function AnimatedList() {
const [items, setItems] = useState([1, 2, 3, 4, 5]);
const addItem = () => {
setItems([...items, items.length + 1]);
};
const removeItem = (id: number) => {
setItems(items.filter((item) => item !== id));
};
return (
<View style={styles.container}>
<Button title="Add Item" onPress={addItem} />
{items.map((item) => (
<Animated.View
key={item}
style={styles.item}
entering={FadeIn.duration(300).springify()}
exiting={SlideOutRight.duration(300)}
layout={Layout.springify()}
>
<Text>Item {item}</Text>
<Pressable onPress={() => removeItem(item)}>
<Text>Remove</Text>
</Pressable>
</Animated.View>
))}
</View>
);
}Swipeable Card
function SwipeableCard({ onSwipeLeft, onSwipeRight }) {
const translateX = useSharedValue(0);
const rotateZ = useSharedValue(0);
const context = useSharedValue({ x: 0 });
const SWIPE_THRESHOLD = 120;
const panGesture = Gesture.Pan()
.onStart(() => {
context.value = { x: translateX.value };
})
.onUpdate((event) => {
translateX.value = event.translationX + context.value.x;
rotateZ.value = interpolate(
translateX.value,
[-200, 0, 200],
[-15, 0, 15]
);
})
.onEnd((event) => {
if (translateX.value > SWIPE_THRESHOLD) {
translateX.value = withTiming(500, { duration: 200 }, () => {
runOnJS(onSwipeRight)();
});
} else if (translateX.value < -SWIPE_THRESHOLD) {
translateX.value = withTiming(-500, { duration: 200 }, () => {
runOnJS(onSwipeLeft)();
});
} else {
translateX.value = withSpring(0);
rotateZ.value = withSpring(0);
}
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ rotate: `${rotateZ.value}deg` },
],
}));
const leftIndicatorStyle = useAnimatedStyle(() => ({
opacity: interpolate(
translateX.value,
[0, SWIPE_THRESHOLD],
[0, 1],
Extrapolation.CLAMP
),
}));
const rightIndicatorStyle = useAnimatedStyle(() => ({
opacity: interpolate(
translateX.value,
[-SWIPE_THRESHOLD, 0],
[1, 0],
Extrapolation.CLAMP
),
}));
return (
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.card, animatedStyle]}>
<Animated.View style={[styles.likeIndicator, leftIndicatorStyle]}>
<Text>LIKE</Text>
</Animated.View>
<Animated.View style={[styles.nopeIndicator, rightIndicatorStyle]}>
<Text>NOPE</Text>
</Animated.View>
{/* Card content */}
</Animated.View>
</GestureDetector>
);
}Bottom Sheet
const MAX_TRANSLATE_Y = -SCREEN_HEIGHT + 50;
const MIN_TRANSLATE_Y = 0;
const SNAP_POINTS = [-SCREEN_HEIGHT * 0.5, -SCREEN_HEIGHT * 0.25, 0];
function BottomSheet({ children }) {
const translateY = useSharedValue(0);
const context = useSharedValue({ y: 0 });
const panGesture = Gesture.Pan()
.onStart(() => {
context.value = { y: translateY.value };
})
.onUpdate((event) => {
translateY.value = clamp(
context.value.y + event.translationY,
MAX_TRANSLATE_Y,
MIN_TRANSLATE_Y
);
})
.onEnd((event) => {
// Find closest snap point
const destination = SNAP_POINTS.reduce((prev, curr) =>
Math.abs(curr - translateY.value) < Math.abs(prev - translateY.value)
? curr
: prev
);
translateY.value = withSpring(destination, {
damping: 50,
stiffness: 300,
});
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateY: translateY.value }],
}));
const backdropStyle = useAnimatedStyle(() => ({
opacity: interpolate(
translateY.value,
[MIN_TRANSLATE_Y, MAX_TRANSLATE_Y],
[0, 0.5]
),
pointerEvents: translateY.value < -50 ? 'auto' : 'none',
}));
return (
<>
<Animated.View style={[styles.backdrop, backdropStyle]} />
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.bottomSheet, animatedStyle]}>
<View style={styles.handle} />
{children}
</Animated.View>
</GestureDetector>
</>
);
}Performance Tips
Memoization
// Memoize animated style when dependencies don't change
const animatedStyle = useAnimatedStyle(
() => ({
transform: [{ translateX: translateX.value }],
}),
[],
); // Empty deps if no external dependencies
// Use useMemo for complex calculations outside worklets
const threshold = useMemo(() => calculateThreshold(screenWidth), [screenWidth]);Worklet Best Practices
// Do: Keep worklets simple
const simpleWorklet = () => {
"worklet";
return scale.value * 2;
};
// Don't: Complex logic in worklets
// Move complex logic to JS with runOnJS
// Do: Use runOnJS for callbacks
const onComplete = () => {
setIsAnimating(false);
};
opacity.value = withTiming(1, {}, (finished) => {
"worklet";
if (finished) {
runOnJS(onComplete)();
}
});Cancel Animations
import { cancelAnimation } from 'react-native-reanimated';
function AnimatedComponent() {
const translateX = useSharedValue(0);
useEffect(() => {
// Start animation
translateX.value = withRepeat(
withTiming(100, { duration: 1000 }),
-1,
true
);
// Cleanup: cancel animation on unmount
return () => {
cancelAnimation(translateX);
};
}, []);
return <Animated.View style={animatedStyle} />;
}React Native Styling Patterns
StyleSheet Fundamentals
Creating Styles
import { StyleSheet, ViewStyle, TextStyle, ImageStyle } from "react-native";
// Typed styles for better IDE support
interface Styles {
container: ViewStyle;
title: TextStyle;
image: ImageStyle;
}
const styles = StyleSheet.create<Styles>({
container: {
flex: 1,
padding: 16,
backgroundColor: "#ffffff",
},
title: {
fontSize: 24,
fontWeight: "700",
color: "#1f2937",
},
image: {
width: 100,
height: 100,
borderRadius: 8,
},
});Combining Styles
import { StyleProp, ViewStyle } from 'react-native';
interface BoxProps {
style?: StyleProp<ViewStyle>;
variant?: 'default' | 'primary' | 'danger';
}
function Box({ style, variant = 'default' }: BoxProps) {
return (
<View
style={[
styles.base,
variant === 'primary' && styles.primary,
variant === 'danger' && styles.danger,
style, // Allow external style overrides
]}
/>
);
}
const styles = StyleSheet.create({
base: {
padding: 16,
borderRadius: 8,
backgroundColor: '#f3f4f6',
},
primary: {
backgroundColor: '#6366f1',
},
danger: {
backgroundColor: '#ef4444',
},
});Theme System
Theme Context
import React, { createContext, useContext, useMemo } from 'react';
import { useColorScheme } from 'react-native';
interface Theme {
colors: {
primary: string;
secondary: string;
background: string;
surface: string;
text: string;
textSecondary: string;
border: string;
error: string;
success: string;
};
spacing: {
xs: number;
sm: number;
md: number;
lg: number;
xl: number;
};
borderRadius: {
sm: number;
md: number;
lg: number;
full: number;
};
typography: {
h1: { fontSize: number; fontWeight: string; lineHeight: number };
h2: { fontSize: number; fontWeight: string; lineHeight: number };
body: { fontSize: number; fontWeight: string; lineHeight: number };
caption: { fontSize: number; fontWeight: string; lineHeight: number };
};
}
const lightTheme: Theme = {
colors: {
primary: '#6366f1',
secondary: '#8b5cf6',
background: '#ffffff',
surface: '#f9fafb',
text: '#1f2937',
textSecondary: '#6b7280',
border: '#e5e7eb',
error: '#ef4444',
success: '#10b981',
},
spacing: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
},
borderRadius: {
sm: 4,
md: 8,
lg: 16,
full: 9999,
},
typography: {
h1: { fontSize: 32, fontWeight: '700', lineHeight: 40 },
h2: { fontSize: 24, fontWeight: '600', lineHeight: 32 },
body: { fontSize: 16, fontWeight: '400', lineHeight: 24 },
caption: { fontSize: 12, fontWeight: '400', lineHeight: 16 },
},
};
const darkTheme: Theme = {
...lightTheme,
colors: {
primary: '#818cf8',
secondary: '#a78bfa',
background: '#111827',
surface: '#1f2937',
text: '#f9fafb',
textSecondary: '#9ca3af',
border: '#374151',
error: '#f87171',
success: '#34d399',
},
};
const ThemeContext = createContext<Theme>(lightTheme);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const colorScheme = useColorScheme();
const theme = useMemo(
() => (colorScheme === 'dark' ? darkTheme : lightTheme),
[colorScheme]
);
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}Using Theme
import { useTheme } from './theme';
function ThemedCard() {
const theme = useTheme();
return (
<View
style={{
backgroundColor: theme.colors.surface,
padding: theme.spacing.md,
borderRadius: theme.borderRadius.lg,
borderWidth: 1,
borderColor: theme.colors.border,
}}
>
<Text
style={{
...theme.typography.h2,
color: theme.colors.text,
marginBottom: theme.spacing.sm,
}}
>
Card Title
</Text>
<Text
style={{
...theme.typography.body,
color: theme.colors.textSecondary,
}}
>
Card description text
</Text>
</View>
);
}Responsive Design
Screen Dimensions
import { Dimensions, useWindowDimensions, PixelRatio } from 'react-native';
// Get dimensions once (may be stale after rotation)
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
// Responsive scaling
const guidelineBaseWidth = 375;
const guidelineBaseHeight = 812;
export const scale = (size: number) =>
(SCREEN_WIDTH / guidelineBaseWidth) * size;
export const verticalScale = (size: number) =>
(SCREEN_HEIGHT / guidelineBaseHeight) * size;
export const moderateScale = (size: number, factor = 0.5) =>
size + (scale(size) - size) * factor;
// Hook for dynamic dimensions (handles rotation)
function ResponsiveComponent() {
const { width, height } = useWindowDimensions();
const isLandscape = width > height;
const isTablet = width >= 768;
return (
<View style={{ flexDirection: isLandscape ? 'row' : 'column' }}>
{/* Content */}
</View>
);
}Breakpoint System
import { useWindowDimensions } from 'react-native';
type Breakpoint = 'sm' | 'md' | 'lg' | 'xl';
const breakpoints = {
sm: 0,
md: 768,
lg: 1024,
xl: 1280,
};
export function useBreakpoint(): Breakpoint {
const { width } = useWindowDimensions();
if (width >= breakpoints.xl) return 'xl';
if (width >= breakpoints.lg) return 'lg';
if (width >= breakpoints.md) return 'md';
return 'sm';
}
export function useResponsiveValue<T>(values: Partial<Record<Breakpoint, T>>): T | undefined {
const breakpoint = useBreakpoint();
const breakpointOrder: Breakpoint[] = ['xl', 'lg', 'md', 'sm'];
const currentIndex = breakpointOrder.indexOf(breakpoint);
for (let i = currentIndex; i < breakpointOrder.length; i++) {
const bp = breakpointOrder[i];
if (values[bp] !== undefined) {
return values[bp];
}
}
return undefined;
}
// Usage
function ResponsiveGrid() {
const columns = useResponsiveValue({ sm: 1, md: 2, lg: 3, xl: 4 }) ?? 1;
return (
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
{items.map((item) => (
<View key={item.id} style={{ width: `${100 / columns}%` }}>
<ItemCard item={item} />
</View>
))}
</View>
);
}Layout Components
Container
import { View, ViewStyle, StyleProp } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from './theme';
interface ContainerProps {
children: React.ReactNode;
style?: StyleProp<ViewStyle>;
edges?: ('top' | 'bottom' | 'left' | 'right')[];
}
export function Container({ children, style, edges = ['top', 'bottom'] }: ContainerProps) {
const insets = useSafeAreaInsets();
const theme = useTheme();
return (
<View
style={[
{
flex: 1,
backgroundColor: theme.colors.background,
paddingTop: edges.includes('top') ? insets.top : 0,
paddingBottom: edges.includes('bottom') ? insets.bottom : 0,
paddingLeft: edges.includes('left') ? insets.left : 0,
paddingRight: edges.includes('right') ? insets.right : 0,
},
style,
]}
>
{children}
</View>
);
}Stack Components
import { View, ViewStyle, StyleProp } from 'react-native';
interface StackProps {
children: React.ReactNode;
spacing?: number;
style?: StyleProp<ViewStyle>;
}
export function VStack({ children, spacing = 8, style }: StackProps) {
return (
<View style={[{ gap: spacing }, style]}>
{children}
</View>
);
}
export function HStack({ children, spacing = 8, style }: StackProps) {
return (
<View style={[{ flexDirection: 'row', alignItems: 'center', gap: spacing }, style]}>
{children}
</View>
);
}
// Usage
function Example() {
return (
<VStack spacing={16}>
<HStack spacing={8}>
<Avatar />
<VStack spacing={2}>
<Text style={styles.name}>John Doe</Text>
<Text style={styles.email}>john@example.com</Text>
</VStack>
</HStack>
<Button title="Edit Profile" />
</VStack>
);
}Spacer
import { View } from 'react-native';
interface SpacerProps {
size?: number;
flex?: number;
}
export function Spacer({ size, flex }: SpacerProps) {
if (flex) {
return <View style={{ flex }} />;
}
return <View style={{ height: size, width: size }} />;
}
// Usage
<HStack>
<Text>Left</Text>
<Spacer flex={1} />
<Text>Right</Text>
</HStack>Shadow Styles
Cross-Platform Shadows
import { Platform, ViewStyle } from "react-native";
export function createShadow(elevation: number, color = "#000000"): ViewStyle {
if (Platform.OS === "android") {
return { elevation };
}
// iOS shadow mapping based on elevation
const shadowMap: Record<number, ViewStyle> = {
1: {
shadowColor: color,
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.18,
shadowRadius: 1,
},
2: {
shadowColor: color,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 2,
},
4: {
shadowColor: color,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.22,
shadowRadius: 4,
},
8: {
shadowColor: color,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.25,
shadowRadius: 8,
},
16: {
shadowColor: color,
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.3,
shadowRadius: 16,
},
};
return shadowMap[elevation] || shadowMap[4];
}
// Predefined shadow styles
export const shadows = {
sm: createShadow(2),
md: createShadow(4),
lg: createShadow(8),
xl: createShadow(16),
};
// Usage
const styles = StyleSheet.create({
card: {
backgroundColor: "#ffffff",
borderRadius: 12,
padding: 16,
...shadows.md,
},
});Typography System
Text Components
import { Text as RNText, TextStyle, StyleProp, TextProps as RNTextProps } from 'react-native';
import { useTheme } from './theme';
type Variant = 'h1' | 'h2' | 'h3' | 'body' | 'bodySmall' | 'caption' | 'label';
type Color = 'primary' | 'secondary' | 'text' | 'textSecondary' | 'error' | 'success';
interface TextProps extends RNTextProps {
variant?: Variant;
color?: Color;
weight?: 'normal' | 'medium' | 'semibold' | 'bold';
align?: 'left' | 'center' | 'right';
}
const variantStyles: Record<Variant, TextStyle> = {
h1: { fontSize: 32, lineHeight: 40, fontWeight: '700' },
h2: { fontSize: 24, lineHeight: 32, fontWeight: '600' },
h3: { fontSize: 20, lineHeight: 28, fontWeight: '600' },
body: { fontSize: 16, lineHeight: 24, fontWeight: '400' },
bodySmall: { fontSize: 14, lineHeight: 20, fontWeight: '400' },
caption: { fontSize: 12, lineHeight: 16, fontWeight: '400' },
label: { fontSize: 14, lineHeight: 20, fontWeight: '500' },
};
const weightStyles: Record<string, TextStyle> = {
normal: { fontWeight: '400' },
medium: { fontWeight: '500' },
semibold: { fontWeight: '600' },
bold: { fontWeight: '700' },
};
export function Text({
variant = 'body',
color = 'text',
weight,
align,
style,
...props
}: TextProps) {
const theme = useTheme();
return (
<RNText
style={[
variantStyles[variant],
{ color: theme.colors[color] },
weight && weightStyles[weight],
align && { textAlign: align },
style,
]}
{...props}
/>
);
}
// Usage
<Text variant="h1">Heading</Text>
<Text variant="body" color="textSecondary">Body text</Text>
<Text variant="label" weight="semibold">Label</Text>Button Styles
Customizable Button
import { Pressable, StyleSheet, ActivityIndicator } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
import { useTheme } from './theme';
type Variant = 'filled' | 'outlined' | 'ghost';
type Size = 'sm' | 'md' | 'lg';
interface ButtonProps {
title: string;
onPress: () => void;
variant?: Variant;
size?: Size;
disabled?: boolean;
loading?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
}
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
export function Button({
title,
onPress,
variant = 'filled',
size = 'md',
disabled = false,
loading = false,
leftIcon,
rightIcon,
}: ButtonProps) {
const theme = useTheme();
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const sizeStyles = {
sm: { paddingVertical: 8, paddingHorizontal: 12, fontSize: 14 },
md: { paddingVertical: 12, paddingHorizontal: 16, fontSize: 16 },
lg: { paddingVertical: 16, paddingHorizontal: 24, fontSize: 18 },
};
const variantStyles = {
filled: {
backgroundColor: theme.colors.primary,
textColor: '#ffffff',
},
outlined: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: theme.colors.primary,
textColor: theme.colors.primary,
},
ghost: {
backgroundColor: 'transparent',
textColor: theme.colors.primary,
},
};
const currentVariant = variantStyles[variant];
const currentSize = sizeStyles[size];
return (
<AnimatedPressable
style={[
styles.base,
{
backgroundColor: currentVariant.backgroundColor,
borderWidth: currentVariant.borderWidth,
borderColor: currentVariant.borderColor,
paddingVertical: currentSize.paddingVertical,
paddingHorizontal: currentSize.paddingHorizontal,
opacity: disabled ? 0.5 : 1,
},
animatedStyle,
]}
onPress={onPress}
onPressIn={() => { scale.value = withSpring(0.97); }}
onPressOut={() => { scale.value = withSpring(1); }}
disabled={disabled || loading}
>
{loading ? (
<ActivityIndicator color={currentVariant.textColor} />
) : (
<>
{leftIcon}
<Text
style={{
color: currentVariant.textColor,
fontSize: currentSize.fontSize,
fontWeight: '600',
marginHorizontal: leftIcon || rightIcon ? 8 : 0,
}}
>
{title}
</Text>
{rightIcon}
</>
)}
</AnimatedPressable>
);
}
const styles = StyleSheet.create({
base: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 8,
},
});Form Styles
Input Component
import { useState } from 'react';
import {
View,
TextInput,
StyleSheet,
TextInputProps,
Pressable,
} from 'react-native';
import { useTheme } from './theme';
interface InputProps extends TextInputProps {
label?: string;
error?: string;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
}
export function Input({
label,
error,
leftIcon,
rightIcon,
style,
...props
}: InputProps) {
const theme = useTheme();
const [isFocused, setIsFocused] = useState(false);
const borderColor = error
? theme.colors.error
: isFocused
? theme.colors.primary
: theme.colors.border;
return (
<View style={styles.container}>
{label && (
<Text style={[styles.label, { color: theme.colors.text }]}>
{label}
</Text>
)}
<View
style={[
styles.inputContainer,
{
borderColor,
backgroundColor: theme.colors.surface,
},
]}
>
{leftIcon && <View style={styles.icon}>{leftIcon}</View>}
<TextInput
style={[
styles.input,
{ color: theme.colors.text },
style,
]}
placeholderTextColor={theme.colors.textSecondary}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
{...props}
/>
{rightIcon && <View style={styles.icon}>{rightIcon}</View>}
</View>
{error && (
<Text style={[styles.error, { color: theme.colors.error }]}>
{error}
</Text>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
marginBottom: 16,
},
label: {
fontSize: 14,
fontWeight: '500',
marginBottom: 6,
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderRadius: 8,
paddingHorizontal: 12,
},
input: {
flex: 1,
paddingVertical: 12,
fontSize: 16,
},
icon: {
marginHorizontal: 4,
},
error: {
fontSize: 12,
marginTop: 4,
},
});List Styles
FlatList with Styling
import { FlatList, View, StyleSheet } from 'react-native';
interface Item {
id: string;
title: string;
subtitle: string;
}
function StyledList({ items }: { items: Item[] }) {
return (
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={({ item, index }) => (
<View
style={[
styles.item,
index === 0 && styles.firstItem,
index === items.length - 1 && styles.lastItem,
]}
>
<Text style={styles.itemTitle}>{item.title}</Text>
<Text style={styles.itemSubtitle}>{item.subtitle}</Text>
</View>
)}
ItemSeparatorComponent={() => <View style={styles.separator} />}
ListHeaderComponent={() => (
<Text style={styles.header}>List Header</Text>
)}
ListEmptyComponent={() => (
<View style={styles.empty}>
<Text>No items found</Text>
</View>
)}
contentContainerStyle={styles.listContent}
/>
);
}
const styles = StyleSheet.create({
listContent: {
padding: 16,
},
item: {
backgroundColor: '#ffffff',
padding: 16,
},
firstItem: {
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
},
lastItem: {
borderBottomLeftRadius: 12,
borderBottomRightRadius: 12,
},
itemTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 4,
},
itemSubtitle: {
fontSize: 14,
color: '#6b7280',
},
separator: {
height: 1,
backgroundColor: '#e5e7eb',
},
header: {
fontSize: 20,
fontWeight: '700',
marginBottom: 16,
},
empty: {
alignItems: 'center',
padding: 32,
},
});Related skills
FAQ
What styling topics does react-native-design cover?
react-native-design teaches StyleSheet.create usage, dynamic styles for themes, Flexbox container layouts, and typography patterns for Text components. Examples include card layouts with title and subtitle hierarchy using explicit color and spacing tokens.
Does react-native-design target iOS and Android?
react-native-design targets cross-platform React Native apps with native-feeling presentation on iOS and Android. It emphasizes maintainable StyleSheet modules and platform-appropriate font sizes, weights, and line heights.
Is React Native Design safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.