Atomic Design component organization pattern for React Native mobile applications. Use when creating new components with proper accessibility and touch targets.
Organizes React Native components using Atomic Design with mobile-first accessibility standards. Use when creating new components to ensure proper touch targets, screen reader support, and platform-specific handling across the five-level hierarchy.
/plugin marketplace add IvanTorresEdge/molcajete.ai/plugin install react-native@Molcajete.aiThis skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill covers the Atomic Design pattern for organizing React Native components with mobile-specific considerations including accessibility, touch targets, and platform differences.
Use this skill when:
ACCESSIBLE BY DEFAULT - Every component must meet mobile accessibility standards including touch targets, screen reader support, and platform conventions.
| 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 |
accessibilityLabel, accessibilityRole, accessibilityStatePlatform.OS or Platform.select when neededexpo-haptics for interactive elementsKeyboardAvoidingView for form inputsreact-native-gesture-handler when neededuseSafeAreaInsets for edge sectionsUse 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 |
// 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',
},
});
// 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,
},
});
// 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,
},
});
// 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,
},
});
// 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>
);
}
// 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,
},
};
// 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
/>
),
};
// 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));
},
},
};
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
// 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';
tsconfig.json:
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}
Note: Expo projects do not use a src/ directory.
// 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';
// 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';
// 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';
// components/templates/index.ts
export { ScreenLayout } from './ScreenLayout';
export { AuthLayout } from './AuthLayout';
export { TabLayout } from './TabLayout';
// components/index.ts
export * from './atoms';
export * from './molecules';
export * from './organisms';
export * from './templates';
accessibilityLabel - descriptive text for screen readersaccessibilityRole - semantic role (button, link, image, etc.)accessibilityState - current state (disabled, selected, checked)accessibilityHint - describes action result (optional but recommended)<Pressable
accessibilityLabel="Submit form"
accessibilityRole="button"
accessibilityState={{ disabled: isDisabled }}
accessibilityHint="Submits the login form"
>
<Text>Submit</Text>
</Pressable>
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
paddingTop: Platform.OS === 'ios' ? 20 : 0,
},
});
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: {},
}),
});
src/ directoryThis skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.