xiaohongshu
xiaohongshu
atomic-design-mobile
Atomic Design component organization pattern for React Native mobile applications. Use when creating new components with proper accessibility and touch targets.
Full skill instructions
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="[email protected]"
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="[email protected]"
/>
),
};
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/ directoryxiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience