name: atomic-design-mobile description: Atomic Design component organization pattern for React Native mobile applications. Use when creating new components with proper accessibility and touch targets.
Atomic Design Mobile Skill
This skill covers the Atomic Design pattern for organizing React Native components with mobile-specific considerations including accessibility, touch targets, and platform differences.
When to Use
Use this skill when:
- Creating new mobile components
- Organizing existing component structures
- Deciding where a component should live
- Ensuring accessibility compliance
- Handling platform-specific requirements
Core Principle
ACCESSIBLE BY DEFAULT - Every component must meet mobile accessibility standards including touch targets, screen reader support, and platform conventions.
The Five-Level Hierarchy
| Level | Alternative Name | Description | Examples | State | Storybook |
|---|---|---|---|---|---|
| Atoms | Elements | Basic building blocks | Button, Input, Text, Icon | Stateless | Yes |
| Molecules | Widgets | Functional units combining atoms | SearchBar, FormField, ListItem | Minimal state | Yes |
| Organisms | Modules | Complex UI sections | Header, TabBar, LoginForm | Can have state | Yes |
| Templates | Layouts | Screen-level layout structures | ScreenLayout, AuthLayout | Layout state only | No |
| Screens | - | Specific template instances | Login screen, Dashboard screen | Full state | No |
Mobile-Specific Requirements by Level
Atoms
- Touch targets: Minimum 44x44pt (Apple HIG, Material Design)
- Accessibility props:
accessibilityLabel,accessibilityRole,accessibilityState - Platform styling: Use
Platform.OSorPlatform.selectwhen needed - Haptic feedback: Consider
expo-hapticsfor interactive elements
Molecules
- Keyboard handling: Use
KeyboardAvoidingViewfor form inputs - Gesture support: Use
react-native-gesture-handlerwhen needed - Safe areas: Consider safe area insets for edge components
Organisms
- Platform awareness: iOS vs Android visual differences
- Safe areas: Use
useSafeAreaInsetsfor edge sections - Navigation integration: Consider navigation context
Templates
- Screen layout: Handle status bar, navigation bar
- Safe areas: Manage all safe area insets
- Keyboard avoidance: Global keyboard handling
- Orientation: Support orientation changes
Component Classification Decision
Use this flowchart to determine the correct atomic level:
| Question | Answer | Level |
|---|---|---|
| Can it be broken down further? | No | Atom |
| Does it combine atoms for a single purpose? | Yes | Molecule |
| Is it a larger section with business logic? | Yes | Organism |
| Does it define screen structure without content? | Yes | Template |
| Does it have real content and data connections? | Yes | Screen |
Classification Checklists
Is it an Atom?
- Cannot be broken down into smaller components
- Single basic element (Pressable, TextInput, Text, Image)
- No business logic
- Stateless or only UI state (pressed, focused)
- No dependencies on other custom components
- Has minimum 44pt touch target
- Has accessibility props
Is it a Molecule?
- Combines 2+ atoms
- Single functional purpose
- Minimal internal state
- No data fetching
- No connection to global state
- Handles keyboard avoidance if containing inputs
Is it an Organism?
- Larger interface section
- May have business logic
- May connect to stores
- Relatively standalone
- Could be used across multiple screens
- Handles safe areas if at screen edges
Is it a Template?
- Defines screen structure
- Uses slots/children for content
- No real data
- Handles safe areas, status bar, keyboard
- Manages screen-level layout concerns
Is it a Screen?
- Uses a template
- Has real content
- Connects to data sources
- Handles routing/navigation
Code Examples
Atom Example
// components/atoms/Button/Button.tsx
import { Pressable, Text, ActivityIndicator, StyleSheet, Platform } from 'react-native';
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
disabled?: boolean;
onPress?: () => void;
children: string;
accessibilityLabel?: string;
}
export function Button({
variant,
size = 'md',
loading,
disabled,
onPress,
children,
accessibilityLabel,
}: ButtonProps) {
return (
<Pressable
style={({ pressed }) => [
styles.base,
styles[variant],
styles[size],
(disabled || loading) && styles.disabled,
pressed && styles.pressed,
]}
onPress={onPress}
disabled={disabled || loading}
accessibilityLabel={accessibilityLabel || children}
accessibilityRole="button"
accessibilityState={{ disabled: disabled || loading }}
>
{loading && <ActivityIndicator color="#fff" style={styles.spinner} />}
<Text style={[styles.text, styles[`${variant}Text`]]}>{children}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
base: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 8,
minHeight: 44, // Minimum touch target
minWidth: 44,
},
primary: {
backgroundColor: '#2563eb',
},
secondary: {
backgroundColor: '#e5e7eb',
},
danger: {
backgroundColor: '#dc2626',
},
sm: {
paddingHorizontal: 12,
paddingVertical: 8,
},
md: {
paddingHorizontal: 16,
paddingVertical: 12,
},
lg: {
paddingHorizontal: 24,
paddingVertical: 16,
},
disabled: {
opacity: 0.5,
},
pressed: {
opacity: 0.8,
},
spinner: {
marginRight: 8,
},
text: {
fontWeight: '600',
},
primaryText: {
color: '#ffffff',
},
secondaryText: {
color: '#111827',
},
dangerText: {
color: '#ffffff',
},
});
Molecule Example
// components/molecules/FormField/FormField.tsx
import { View, Text, TextInput, StyleSheet } from 'react-native';
interface FormFieldProps {
label: string;
value: string;
onChangeText: (text: string) => void;
placeholder?: string;
error?: string;
required?: boolean;
secureTextEntry?: boolean;
keyboardType?: 'default' | 'email-address' | 'numeric' | 'phone-pad';
accessibilityLabel?: string;
}
export function FormField({
label,
value,
onChangeText,
placeholder,
error,
required,
secureTextEntry,
keyboardType = 'default',
accessibilityLabel,
}: FormFieldProps) {
const inputAccessibilityLabel = accessibilityLabel || `${label}${required ? ', required' : ''}`;
return (
<View style={styles.container}>
<Text style={styles.label}>
{label}
{required && <Text style={styles.required}> *</Text>}
</Text>
<TextInput
style={[styles.input, error && styles.inputError]}
value={value}
onChangeText={onChangeText}
placeholder={placeholder}
secureTextEntry={secureTextEntry}
keyboardType={keyboardType}
accessibilityLabel={inputAccessibilityLabel}
accessibilityState={{ disabled: false }}
accessibilityHint={error}
/>
{error && (
<Text style={styles.error} accessibilityRole="alert">
{error}
</Text>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
marginBottom: 16,
},
label: {
fontSize: 14,
fontWeight: '500',
color: '#374151',
marginBottom: 4,
},
required: {
color: '#dc2626',
},
input: {
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 12,
fontSize: 16,
minHeight: 44, // Minimum touch target
},
inputError: {
borderColor: '#dc2626',
},
error: {
fontSize: 12,
color: '#dc2626',
marginTop: 4,
},
});
Organism Example
// components/organisms/LoginForm/LoginForm.tsx
import { useState } from 'react';
import { View, StyleSheet, KeyboardAvoidingView, Platform } from 'react-native';
import { Button } from '@/components/atoms';
import { FormField } from '@/components/molecules';
interface LoginFormProps {
onSubmit: (email: string, password: string) => Promise<void>;
}
export function LoginForm({ onSubmit }: LoginFormProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const validate = (): boolean => {
const newErrors: Record<string, string> = {};
if (!email) newErrors.email = 'Email is required';
if (!password) newErrors.password = 'Password is required';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async () => {
if (!validate()) return;
setLoading(true);
try {
await onSubmit(email, password);
} catch {
setErrors({ form: 'Invalid credentials' });
} finally {
setLoading(false);
}
};
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.container}
>
<FormField
label="Email"
value={email}
onChangeText={setEmail}
placeholder="you@example.com"
keyboardType="email-address"
error={errors.email}
required
/>
<FormField
label="Password"
value={password}
onChangeText={setPassword}
placeholder="Enter password"
secureTextEntry
error={errors.password}
required
/>
<Button
variant="primary"
onPress={handleSubmit}
loading={loading}
accessibilityLabel="Sign in"
>
Sign In
</Button>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
padding: 16,
},
});
Template Example
// components/templates/ScreenLayout/ScreenLayout.tsx
import { SafeAreaView, View, StyleSheet, StatusBar, Platform } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Header } from '@/components/organisms';
interface ScreenLayoutProps {
children: React.ReactNode;
title?: string;
showHeader?: boolean;
showBackButton?: boolean;
onBack?: () => void;
}
export function ScreenLayout({
children,
title,
showHeader = true,
showBackButton = false,
onBack,
}: ScreenLayoutProps) {
const insets = useSafeAreaInsets();
return (
<View style={[styles.container, { paddingTop: insets.top }]}>
<StatusBar barStyle="dark-content" />
{showHeader && (
<Header
title={title}
showBackButton={showBackButton}
onBack={onBack}
/>
)}
<View style={[styles.content, { paddingBottom: insets.bottom }]}>
{children}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#ffffff',
},
content: {
flex: 1,
padding: 16,
},
});
Screen Example (Expo Router)
// app/(auth)/login.tsx
import { useRouter } from 'expo-router';
import { AuthLayout } from '@/components/templates';
import { LoginForm } from '@/components/organisms';
import { useAuth } from '@/hooks/useAuth';
export default function LoginScreen() {
const router = useRouter();
const { login } = useAuth();
const handleLogin = async (email: string, password: string) => {
await login(email, password);
router.replace('/(tabs)');
};
return (
<AuthLayout title="Welcome Back" subtitle="Sign in to your account">
<LoginForm onSubmit={handleLogin} />
</AuthLayout>
);
}
React Native Storybook Story Templates
Atom Story Template
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-native';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Atoms/Button',
component: Button,
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
loading: { control: 'boolean' },
disabled: { control: 'boolean' },
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Primary Button',
},
};
export const Secondary: Story = {
args: {
variant: 'secondary',
children: 'Secondary Button',
},
};
export const Loading: Story = {
args: {
variant: 'primary',
children: 'Saving...',
loading: true,
},
};
export const Disabled: Story = {
args: {
variant: 'primary',
children: 'Disabled',
disabled: true,
},
};
Molecule Story Template
// FormField.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-native';
import { useState } from 'react';
import { FormField } from './FormField';
const meta: Meta<typeof FormField> = {
title: 'Molecules/FormField',
component: FormField,
};
export default meta;
type Story = StoryObj<typeof FormField>;
// Wrapper for controlled input
function FormFieldWrapper(props: any) {
const [value, setValue] = useState('');
return <FormField {...props} value={value} onChangeText={setValue} />;
}
export const Default: Story = {
render: () => (
<FormFieldWrapper
label="Email"
placeholder="you@example.com"
/>
),
};
export const WithError: Story = {
render: () => (
<FormFieldWrapper
label="Email"
error="Email is required"
required
/>
),
};
export const Password: Story = {
render: () => (
<FormFieldWrapper
label="Password"
placeholder="Enter password"
secureTextEntry
required
/>
),
};
Organism Story Template
// LoginForm.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-native';
import { LoginForm } from './LoginForm';
const meta: Meta<typeof LoginForm> = {
title: 'Organisms/LoginForm',
component: LoginForm,
};
export default meta;
type Story = StoryObj<typeof LoginForm>;
export const Default: Story = {
args: {
onSubmit: async (email, password) => {
console.log('Login:', { email, password });
await new Promise((resolve) => setTimeout(resolve, 1000));
},
},
};
Naming Conventions
components/
├── atoms/
│ ├── Button/ # PascalCase - noun
│ ├── Input/
│ ├── Text/
│ └── Icon/
├── molecules/
│ ├── SearchBar/ # PascalCase - descriptive compound
│ ├── FormField/
│ └── ListItem/
├── organisms/
│ ├── Header/ # PascalCase - section name
│ ├── TabBar/
│ └── LoginForm/
├── templates/
│ ├── ScreenLayout/ # PascalCase - always end with "Layout"
│ ├── AuthLayout/
│ └── TabLayout/
└── index.ts
app/ # Screens via Expo Router
├── (auth)/
│ ├── login.tsx # lowercase - Expo Router convention
│ └── register.tsx
└── (tabs)/
├── index.tsx
└── profile.tsx
Import Strategy
// Within same level - use relative imports
import { Button } from '../Button';
// Across levels - use path alias (no src/ prefix for Expo)
import { Button, Input } from '@/components/atoms';
import { SearchBar, FormField } from '@/components/molecules';
import { Header, LoginForm } from '@/components/organisms';
import { ScreenLayout, AuthLayout } from '@/components/templates';
// From top-level barrel
import { Button, Input, SearchBar, Header } from '@/components';
Path Alias Configuration (Expo)
tsconfig.json:
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}
Note: Expo projects do not use a src/ directory.
Barrel Export Patterns
Atom Level Barrel Export
// components/atoms/index.ts
export { Button } from './Button';
export { Input } from './Input';
export { Text } from './Text';
export { Icon } from './Icon';
export { Avatar } from './Avatar';
export { Spinner } from './Spinner';
// Re-export types
export type { ButtonProps } from './Button';
export type { InputProps } from './Input';
Molecule Level Barrel Export
// components/molecules/index.ts
export { SearchBar } from './SearchBar';
export { FormField } from './FormField';
export { ListItem } from './ListItem';
export { Card } from './Card';
export type { FormFieldProps } from './FormField';
Organism Level Barrel Export
// components/organisms/index.ts
export { Header } from './Header';
export { TabBar } from './TabBar';
export { LoginForm } from './LoginForm';
export { BottomSheet } from './BottomSheet';
export type { LoginFormProps } from './LoginForm';
Template Level Barrel Export
// components/templates/index.ts
export { ScreenLayout } from './ScreenLayout';
export { AuthLayout } from './AuthLayout';
export { TabLayout } from './TabLayout';
Main Barrel Export
// components/index.ts
export * from './atoms';
export * from './molecules';
export * from './organisms';
export * from './templates';
Accessibility Checklist
Every Atom Must Have
-
accessibilityLabel- descriptive text for screen readers -
accessibilityRole- semantic role (button, link, image, etc.) -
accessibilityState- current state (disabled, selected, checked) - Minimum 44x44pt touch target
- Visible focus indicator (where applicable)
Every Interactive Element Must Have
-
accessibilityHint- describes action result (optional but recommended) - Proper contrast ratio (4.5:1 for text)
- Touch feedback (pressed state visual change)
Example Accessibility Props
<Pressable
accessibilityLabel="Submit form"
accessibilityRole="button"
accessibilityState={{ disabled: isDisabled }}
accessibilityHint="Submits the login form"
>
<Text>Submit</Text>
</Pressable>
Platform-Specific Patterns
Using Platform.OS
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
paddingTop: Platform.OS === 'ios' ? 20 : 0,
},
});
Using Platform.select
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
shadow: Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 4,
},
android: {
elevation: 4,
},
default: {},
}),
});
Best Practices
- Touch targets first - Every interactive element is at least 44x44pt
- Accessibility always - Never skip accessibilityLabel and accessibilityRole
- Use StyleSheet - Create styles outside components for performance
- Pressable over TouchableOpacity - Better accessibility support
- Safe areas everywhere - Handle notches, home indicators, status bars
- Keyboard avoidance - Wrap forms in KeyboardAvoidingView
- Platform awareness - Test on both iOS and Android
- Test with VoiceOver/TalkBack - Verify screen reader experience
Notes
- Expo projects do not use a
src/directory - Templates and Screens do not get Storybook stories
- React Native Storybook runs on-device, not in browser
- Brad Frost's original article: https://bradfrost.com/blog/post/atomic-web-design/