Claude Code Plugins

Community-maintained marketplace

Feedback

atomic-design-mobile

@IvanTorresEdge/molcajete.ai
0
0

Atomic Design component organization pattern for React Native mobile applications. Use when creating new components with proper accessibility and touch targets.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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.OS or Platform.select when needed
  • Haptic feedback: Consider expo-haptics for interactive elements

Molecules

  • Keyboard handling: Use KeyboardAvoidingView for form inputs
  • Gesture support: Use react-native-gesture-handler when needed
  • Safe areas: Consider safe area insets for edge components

Organisms

  • Platform awareness: iOS vs Android visual differences
  • Safe areas: Use useSafeAreaInsets for 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

  1. Touch targets first - Every interactive element is at least 44x44pt
  2. Accessibility always - Never skip accessibilityLabel and accessibilityRole
  3. Use StyleSheet - Create styles outside components for performance
  4. Pressable over TouchableOpacity - Better accessibility support
  5. Safe areas everywhere - Handle notches, home indicators, status bars
  6. Keyboard avoidance - Wrap forms in KeyboardAvoidingView
  7. Platform awareness - Test on both iOS and Android
  8. Test with VoiceOver/TalkBack - Verify screen reader experience

Notes