Claude-skill-registry atomic-design-mobile

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

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/atomic-design-mobile" ~/.claude/skills/majiayu000-claude-skill-registry-atomic-design-mobile && rm -rf "$T"
manifest: skills/data/atomic-design-mobile/SKILL.md
source content

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

LevelAlternative NameDescriptionExamplesStateStorybook
AtomsElementsBasic building blocksButton, Input, Text, IconStatelessYes
MoleculesWidgetsFunctional units combining atomsSearchBar, FormField, ListItemMinimal stateYes
OrganismsModulesComplex UI sectionsHeader, TabBar, LoginFormCan have stateYes
TemplatesLayoutsScreen-level layout structuresScreenLayout, AuthLayoutLayout state onlyNo
Screens-Specific template instancesLogin screen, Dashboard screenFull stateNo

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:

QuestionAnswerLevel
Can it be broken down further?NoAtom
Does it combine atoms for a single purpose?YesMolecule
Is it a larger section with business logic?YesOrganism
Does it define screen structure without content?YesTemplate
Does it have real content and data connections?YesScreen

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