Atomic Design component organization pattern for React applications. Use when creating new components or organizing component structures.
Organizes React components into a five-level hierarchy (atoms, molecules, organisms, templates, pages) for reusability and scalability. Use this pattern when creating new components or deciding where they belong in your project structure.
/plugin marketplace add IvanTorresEdge/molcajete.ai/plugin install react@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 components into a clear hierarchy that promotes reusability, consistency, and scalability.
Use this skill when:
COMPOSITION FROM SIMPLE TO COMPLEX - Build complex interfaces by composing simple, well-tested building blocks.
| Level | Alternative Name | Description | Examples | State | Storybook |
|---|---|---|---|---|---|
| Atoms | Elements | Basic building blocks | Button, Input, Label, Icon | Stateless | Yes |
| Molecules | Widgets | Functional units combining atoms | SearchForm, FormField, Card | Minimal state | Yes |
| Organisms | Modules | Complex UI sections | Header, Footer, LoginForm | Can have state | Yes |
| Templates | Layouts | Page-level layout structures | MainLayout, AuthLayout | Layout state only | No |
| Pages | - | Specific template instances | HomePage, DashboardPage | Full state | No |
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 page structure without content? | Yes | Template |
| Does it have real content and data connections? | Yes | Page |
// src/components/atoms/Button/Button.tsx
import { forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant: 'primary' | 'secondary' | 'danger' | 'ghost';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant, size = 'md', loading, className, children, disabled, ...props }, ref) => {
return (
<button
ref={ref}
type="button"
className={cn(
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
'focus-visible:outline-none focus-visible:ring-2',
{
'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
'bg-gray-200 text-gray-900 hover:bg-gray-300': variant === 'secondary',
'bg-red-600 text-white hover:bg-red-700': variant === 'danger',
'bg-transparent hover:bg-gray-100': variant === 'ghost',
'px-3 py-1.5 text-sm': size === 'sm',
'px-4 py-2 text-base': size === 'md',
'px-6 py-3 text-lg': size === 'lg',
'opacity-50 cursor-not-allowed': disabled || loading,
},
className
)}
disabled={disabled || loading}
{...props}
>
{loading && <span className="mr-2 h-4 w-4 animate-spin">...</span>}
{children}
</button>
);
}
);
Button.displayName = 'Button';
// src/components/molecules/FormField/FormField.tsx
import { Label, Input, Text } from '@/components/atoms';
interface FormFieldProps {
label: string;
name: string;
type?: string;
placeholder?: string;
error?: string;
required?: boolean;
value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
export function FormField({
label,
name,
type = 'text',
placeholder,
error,
required,
value,
onChange,
}: FormFieldProps): React.ReactElement {
return (
<div className="space-y-1">
<Label htmlFor={name}>
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</Label>
<Input
id={name}
name={name}
type={type}
placeholder={placeholder}
value={value}
onChange={onChange}
aria-invalid={!!error}
aria-describedby={error ? `${name}-error` : undefined}
/>
{error && (
<Text id={`${name}-error`} className="text-red-500 text-sm">
{error}
</Text>
)}
</div>
);
}
// src/components/organisms/LoginForm/LoginForm.tsx
import { useState } from 'react';
import { Button } from '@/components/atoms';
import { FormField } from '@/components/molecules';
interface LoginFormProps {
onSubmit: (email: string, password: string) => Promise<void>;
onForgotPassword?: () => void;
}
export function LoginForm({ onSubmit, onForgotPassword }: LoginFormProps): React.ReactElement {
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 (e: React.FormEvent): Promise<void> => {
e.preventDefault();
if (!validate()) return;
setLoading(true);
try {
await onSubmit(email, password);
} catch {
setErrors({ form: 'Invalid credentials' });
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4 max-w-md">
<FormField
label="Email"
name="email"
type="email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
error={errors.email}
required
/>
<FormField
label="Password"
name="password"
type="password"
placeholder="********"
value={password}
onChange={(e) => setPassword(e.target.value)}
error={errors.password}
required
/>
{errors.form && <div className="text-red-500 text-sm">{errors.form}</div>}
<Button variant="primary" type="submit" loading={loading} className="w-full">
Sign In
</Button>
{onForgotPassword && (
<button
type="button"
onClick={onForgotPassword}
className="text-sm text-blue-600 hover:underline"
>
Forgot password?
</button>
)}
</form>
);
}
// src/components/templates/AuthLayout/AuthLayout.tsx
interface AuthLayoutProps {
children: React.ReactNode;
title: string;
subtitle?: string;
}
export function AuthLayout({ children, title, subtitle }: AuthLayoutProps): React.ReactElement {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
<div className="text-center">
<h1 className="text-3xl font-bold">{title}</h1>
{subtitle && <p className="mt-2 text-gray-600">{subtitle}</p>}
</div>
{children}
</div>
</div>
);
}
// src/pages/Login/LoginPage.tsx
import { useNavigate } from 'react-router-dom';
import { AuthLayout } from '@/components/templates';
import { LoginForm } from '@/components/organisms';
import { useAuth } from '@/hooks/useAuth';
export function LoginPage(): React.ReactElement {
const navigate = useNavigate();
const { login } = useAuth();
const handleLogin = async (email: string, password: string) => {
await login(email, password);
navigate('/dashboard');
};
return (
<AuthLayout title="Welcome Back" subtitle="Sign in to your account">
<LoginForm onSubmit={handleLogin} onForgotPassword={() => navigate('/forgot-password')} />
</AuthLayout>
);
}
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Atoms/Button',
component: Button,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger', 'ghost'],
description: 'The visual style of the button',
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
description: 'The size of the button',
},
loading: {
control: 'boolean',
description: 'Shows a loading spinner',
},
disabled: {
control: 'boolean',
description: 'Disables the button',
},
},
};
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 AllVariants: Story = {
render: () => (
<div className="flex gap-4">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="danger">Danger</Button>
<Button variant="ghost">Ghost</Button>
</div>
),
};
// FormField.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { FormField } from './FormField';
const meta: Meta<typeof FormField> = {
title: 'Molecules/FormField',
component: FormField,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
};
export default meta;
type Story = StoryObj<typeof FormField>;
export const Default: Story = {
args: {
label: 'Email',
name: 'email',
placeholder: 'you@example.com',
},
};
export const WithError: Story = {
args: {
label: 'Email',
name: 'email',
error: 'Email is required',
required: true,
},
};
export const Required: Story = {
args: {
label: 'Password',
name: 'password',
type: 'password',
required: true,
},
};
// LoginForm.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { LoginForm } from './LoginForm';
const meta: Meta<typeof LoginForm> = {
title: 'Organisms/LoginForm',
component: LoginForm,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
};
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));
},
onForgotPassword: () => console.log('Forgot password clicked'),
},
};
export const WithoutForgotPassword: Story = {
args: {
onSubmit: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
},
},
};
atoms/
Button/ # PascalCase - noun (what it is)
Input/
Icon/
molecules/
SearchForm/ # PascalCase - descriptive compound name
InputGroup/
FormField/
organisms/
Header/ # PascalCase - section name
LoginForm/
ProductCard/
templates/
MainLayout/ # PascalCase - always end with "Layout"
DashboardLayout/
AuthLayout/
pages/
HomePage/ # PascalCase - always end with "Page"
DashboardPage/
ProfilePage/
// Within same level - use relative imports
import { Button } from '../Button';
// Across levels - use path alias
import { Button, Input } from '@/components/atoms';
import { SearchForm, FormField } from '@/components/molecules';
import { Header, LoginForm } from '@/components/organisms';
import { MainLayout, AuthLayout } from '@/components/templates';
// From top-level barrel (when importing many components)
import { Button, Input, SearchForm, Header } from '@/components';
Vite (vite.config.ts):
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
Next.js (tsconfig.json):
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
// src/components/atoms/index.ts
export { Button } from './Button';
export { Input } from './Input';
export { Label } from './Label';
export { Icon } from './Icon';
export { Text } from './Text';
export { Image } from './Image';
export { Badge } from './Badge';
export { Avatar } from './Avatar';
export { Spinner } from './Spinner';
// Re-export types
export type { ButtonProps } from './Button';
export type { InputProps } from './Input';
// src/components/molecules/index.ts
export { SearchForm } from './SearchForm';
export { InputGroup } from './InputGroup';
export { Card } from './Card';
export { FormField } from './FormField';
export { MenuItem } from './MenuItem';
export type { FormFieldProps } from './FormField';
// src/components/organisms/index.ts
export { Header } from './Header';
export { Footer } from './Footer';
export { Navigation } from './Navigation';
export { Sidebar } from './Sidebar';
export { LoginForm } from './LoginForm';
export type { LoginFormProps } from './LoginForm';
// src/components/templates/index.ts
export { MainLayout } from './MainLayout';
export { DashboardLayout } from './DashboardLayout';
export { AuthLayout } from './AuthLayout';
// src/components/index.ts
export * from './atoms';
export * from './molecules';
export * from './organisms';
export * from './templates';
Projects can opt out of Atomic Design by specifying in their PRD:
# .molcajete/prd/tech-stack.yaml or tech-stack.md frontmatter
techStack:
framework: react
componentOrganization: atomic # Default - Atomic Design
# componentOrganization: flat # Simple flat structure
# componentOrganization: feature-based # Feature modules
When componentOrganization: flat:
src/
├── components/
│ ├── Button/
│ ├── Input/
│ ├── Header/
│ ├── LoginForm/
│ └── ...
When componentOrganization: feature-based:
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ ├── hooks/
│ │ └── stores/
│ ├── dashboard/
│ └── profile/
├── shared/
│ └── components/
│ ├── Button/
│ └── Input/
This 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.