--- name: atomic-design-organisms description: Use when building complex organisms from molecules and atoms like headers, footers, product cards, and sidebars. Organisms are distinct UI sections. allowed-tools: - Bash - Read - Write - Edit - Glob - Grep --- # Atomic Design: Organisms Master the creation of organisms - complex, distinct sections of an interface composed of molecules and atoms. Organisms represent standalone UI sections that could exist independently. ## What Are Organisms? Organisms are relatively complex UI components that form distinct sections of an interface. They are: - **Composed of molecules and atoms**: May include both levels - **Standalone sections**: Can exist independently on a page - **Context-aware**: Often tied to specific business contexts - **Stateful**: May manage significant internal state - **Reusable**: Used across different templates and pages ## Common Organism Types ### Navigation Organisms - Header (logo + navigation + user menu) - Footer (links + social icons + legal) - Sidebar (navigation + user info + actions) - Breadcrumbs (full navigation path) ### Content Organisms - Product cards (image + details + actions) - Comment sections (comments + reply forms) - Article previews (title + excerpt + meta) - User profiles (avatar + bio + stats) ### Form Organisms - Login forms (fields + actions + links) - Registration forms (multi-step fields) - Checkout forms (payment + shipping) - Search with filters ### Data Display Organisms - Data tables (header + rows + pagination) - Dashboards (stats + charts + actions) - Timelines (events + connectors) - Galleries (images + navigation) ## Header Organism Example ### Complete Implementation ```typescript // organisms/Header/Header.tsx import React, { useState } from 'react'; import { Icon } from '@/components/atoms/Icon'; import { Button } from '@/components/atoms/Button'; import { Avatar } from '@/components/atoms/Avatar'; import { NavItem } from '@/components/molecules/NavItem'; import { SearchForm } from '@/components/molecules/SearchForm'; import styles from './Header.module.css'; export interface NavLink { id: string; label: string; href: string; icon?: string; badge?: number; } export interface User { id: string; name: string; email: string; avatar?: string; } export interface HeaderProps { /** Logo element or image */ logo: React.ReactNode; /** Navigation links */ navigation: NavLink[]; /** Current active nav item */ activeNavId?: string; /** Authenticated user */ user?: User | null; /** Show search form */ showSearch?: boolean; /** Search submit handler */ onSearch?: (query: string) => void; /** Login click handler */ onLogin?: () => void; /** Logout click handler */ onLogout?: () => void; /** Profile click handler */ onProfileClick?: () => void; } export const Header: React.FC = ({ logo, navigation, activeNavId, user, showSearch = true, onSearch, onLogin, onLogout, onProfileClick, }) => { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [userMenuOpen, setUserMenuOpen] = useState(false); return (
{/* Logo */}
{logo}
{/* Desktop Navigation */} {/* Search */} {showSearch && onSearch && (
)} {/* User Actions */}
{user ? (
{userMenuOpen && (
)}
) : ( )}
{/* Mobile Menu Toggle */}
{/* Mobile Navigation */} {mobileMenuOpen && ( )}
); }; Header.displayName = 'Header'; ``` ```css /* organisms/Header/Header.module.css */ .header { position: sticky; top: 0; z-index: 100; background-color: var(--color-white); border-bottom: 1px solid var(--color-neutral-200); } .container { display: flex; align-items: center; gap: 24px; max-width: 1280px; margin: 0 auto; padding: 12px 24px; } .logo { flex-shrink: 0; } .nav { display: none; flex: 1; } @media (min-width: 768px) { .nav { display: block; } } .navList { display: flex; gap: 8px; list-style: none; margin: 0; padding: 0; } .search { display: none; width: 280px; } @media (min-width: 1024px) { .search { display: block; } } .actions { display: flex; align-items: center; gap: 12px; } .userMenu { position: relative; } .userButton { display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: transparent; border: 1px solid var(--color-neutral-200); border-radius: 6px; cursor: pointer; } .userName { display: none; } @media (min-width: 640px) { .userName { display: inline; } } .dropdown { position: absolute; top: 100%; right: 0; margin-top: 8px; min-width: 160px; background: var(--color-white); border: 1px solid var(--color-neutral-200); border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); overflow: hidden; } .dropdown button { display: flex; align-items: center; gap: 8px; width: 100%; padding: 12px 16px; background: transparent; border: none; cursor: pointer; text-align: left; } .dropdown button:hover { background-color: var(--color-neutral-50); } .mobileToggle { display: flex; padding: 8px; background: transparent; border: none; cursor: pointer; } @media (min-width: 768px) { .mobileToggle { display: none; } } .mobileNav { border-top: 1px solid var(--color-neutral-200); padding: 16px; } .mobileNav ul { display: flex; flex-direction: column; gap: 8px; list-style: none; margin: 0; padding: 0; } ``` ## Footer Organism Example ```typescript // organisms/Footer/Footer.tsx import React from 'react'; import { Icon } from '@/components/atoms/Icon'; import { Text } from '@/components/atoms/Typography'; import styles from './Footer.module.css'; export interface FooterLink { label: string; href: string; } export interface FooterSection { title: string; links: FooterLink[]; } export interface SocialLink { platform: string; href: string; icon: string; } export interface FooterProps { /** Footer logo */ logo: React.ReactNode; /** Tagline or description */ tagline?: string; /** Link sections */ sections: FooterSection[]; /** Social media links */ socialLinks?: SocialLink[]; /** Copyright text */ copyright?: string; /** Legal links */ legalLinks?: FooterLink[]; } export const Footer: React.FC = ({ logo, tagline, sections, socialLinks = [], copyright, legalLinks = [], }) => { const currentYear = new Date().getFullYear(); const copyrightText = copyright || `${currentYear} Company. All rights reserved.`; return (
{/* Brand Column */}
{logo}
{tagline && ( {tagline} )} {socialLinks.length > 0 && (
{socialLinks.map((link) => ( ))}
)}
{/* Link Sections */}
{sections.map((section) => (
{section.title}
))}
{/* Bottom Bar */}
{copyrightText} {legalLinks.length > 0 && (
{legalLinks.map((link, index) => ( {index > 0 && |} {link.label} ))}
)}
); }; Footer.displayName = 'Footer'; ``` ## ProductCard Organism Example ```typescript // organisms/ProductCard/ProductCard.tsx import React from 'react'; import { Button } from '@/components/atoms/Button'; import { Badge } from '@/components/atoms/Badge'; import { Icon } from '@/components/atoms/Icon'; import { Text, Heading } from '@/components/atoms/Typography'; import styles from './ProductCard.module.css'; export interface Product { id: string; name: string; description?: string; price: number; originalPrice?: number; currency?: string; image: string; rating?: number; reviewCount?: number; inStock?: boolean; badge?: string; } export interface ProductCardProps { /** Product data */ product: Product; /** Add to cart handler */ onAddToCart?: (productId: string) => void; /** Quick view handler */ onQuickView?: (productId: string) => void; /** Favorite toggle handler */ onFavorite?: (productId: string) => void; /** Whether product is favorited */ isFavorite?: boolean; /** Loading state for add to cart */ isAddingToCart?: boolean; } export const ProductCard: React.FC = ({ product, onAddToCart, onQuickView, onFavorite, isFavorite = false, isAddingToCart = false, }) => { const { id, name, description, price, originalPrice, currency = '$', image, rating, reviewCount, inStock = true, badge, } = product; const discount = originalPrice ? Math.round(((originalPrice - price) / originalPrice) * 100) : null; const formatPrice = (value: number) => { return `${currency}${value.toFixed(2)}`; }; return (
{/* Image Section */}
{name} {badge && ( {badge} )} {discount && ( -{discount}% )} {/* Quick Actions Overlay */}
{onFavorite && ( )} {onQuickView && ( )}
{/* Content Section */}
{name} {description && ( {description} )} {/* Rating */} {rating !== undefined && (
{[1, 2, 3, 4, 5].map((star) => ( ))}
{reviewCount !== undefined && ( ({reviewCount}) )}
)} {/* Price */}
{formatPrice(price)} {originalPrice && ( {formatPrice(originalPrice)} )}
{/* Actions */}
{inStock ? ( ) : ( )}
); }; ProductCard.displayName = 'ProductCard'; ``` ## CommentSection Organism Example ```typescript // organisms/CommentSection/CommentSection.tsx import React, { useState } from 'react'; import { Button } from '@/components/atoms/Button'; import { Heading, Text } from '@/components/atoms/Typography'; import { MediaObject } from '@/components/molecules/MediaObject'; import { FormField } from '@/components/molecules/FormField'; import styles from './CommentSection.module.css'; export interface Comment { id: string; author: { name: string; avatar?: string; }; content: string; createdAt: string; likes: number; replies?: Comment[]; } export interface CommentSectionProps { /** Comments list */ comments: Comment[]; /** Total comment count */ totalCount: number; /** Current user (for comment form) */ currentUser?: { name: string; avatar?: string }; /** Submit comment handler */ onSubmit?: (content: string, parentId?: string) => void; /** Like comment handler */ onLike?: (commentId: string) => void; /** Delete comment handler */ onDelete?: (commentId: string) => void; /** Loading state */ isLoading?: boolean; } export const CommentSection: React.FC = ({ comments, totalCount, currentUser, onSubmit, onLike, onDelete, isLoading = false, }) => { const [newComment, setNewComment] = useState(''); const [replyingTo, setReplyingTo] = useState(null); const [replyContent, setReplyContent] = useState(''); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (newComment.trim() && onSubmit) { onSubmit(newComment.trim()); setNewComment(''); } }; const handleReply = (parentId: string) => { if (replyContent.trim() && onSubmit) { onSubmit(replyContent.trim(), parentId); setReplyContent(''); setReplyingTo(null); } }; const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }); }; const renderComment = (comment: Comment, isReply = false) => (
{comment.author.name} } subtitle={formatDate(comment.createdAt)} />
{comment.content}
{!isReply && currentUser && ( )} {onDelete && ( )}
{/* Reply Form */} {replyingTo === comment.id && (
setReplyContent(e.target.value)} />
)} {/* Nested Replies */} {comment.replies && comment.replies.length > 0 && (
{comment.replies.map((reply) => renderComment(reply, true))}
)}
); return (
Comments ({totalCount}) {/* New Comment Form */} {currentUser && onSubmit && (
setNewComment(e.target.value)} /> } />
)} {/* Comments List */}
{comments.length > 0 ? ( comments.map((comment) => renderComment(comment)) ) : ( No comments yet. Be the first to comment! )}
); }; CommentSection.displayName = 'CommentSection'; ``` ## Sidebar Organism Example ```typescript // organisms/Sidebar/Sidebar.tsx import React from 'react'; import { Avatar } from '@/components/atoms/Avatar'; import { Text } from '@/components/atoms/Typography'; import { NavItem } from '@/components/molecules/NavItem'; import styles from './Sidebar.module.css'; export interface SidebarLink { id: string; label: string; href: string; icon: string; badge?: number; } export interface SidebarSection { title?: string; links: SidebarLink[]; } export interface SidebarProps { /** User info */ user?: { name: string; email: string; avatar?: string; }; /** Navigation sections */ sections: SidebarSection[]; /** Active link ID */ activeId?: string; /** Collapsed state */ isCollapsed?: boolean; /** Toggle collapse handler */ onToggleCollapse?: () => void; } export const Sidebar: React.FC = ({ user, sections, activeId, isCollapsed = false, onToggleCollapse, }) => { return ( ); }; Sidebar.displayName = 'Sidebar'; ``` ## DataTable Organism Example ```typescript // organisms/DataTable/DataTable.tsx import React, { useState } from 'react'; import { Checkbox } from '@/components/atoms/Checkbox'; import { Button } from '@/components/atoms/Button'; import { Icon } from '@/components/atoms/Icon'; import { Text } from '@/components/atoms/Typography'; import { ListItem } from '@/components/molecules/ListItem'; import styles from './DataTable.module.css'; export interface Column { id: string; header: string; accessor: keyof T | ((row: T) => React.ReactNode); sortable?: boolean; width?: string; } export interface DataTableProps { /** Table columns */ columns: Column[]; /** Table data */ data: T[]; /** Selected row IDs */ selectedIds?: string[]; /** Selection change handler */ onSelectionChange?: (ids: string[]) => void; /** Sort field */ sortField?: string; /** Sort direction */ sortDirection?: 'asc' | 'desc'; /** Sort change handler */ onSort?: (field: string) => void; /** Row click handler */ onRowClick?: (row: T) => void; /** Loading state */ isLoading?: boolean; /** Empty state message */ emptyMessage?: string; } export function DataTable({ columns, data, selectedIds = [], onSelectionChange, sortField, sortDirection, onSort, onRowClick, isLoading = false, emptyMessage = 'No data available', }: DataTableProps) { const allSelected = data.length > 0 && selectedIds.length === data.length; const someSelected = selectedIds.length > 0 && !allSelected; const handleSelectAll = () => { if (onSelectionChange) { onSelectionChange(allSelected ? [] : data.map((row) => row.id)); } }; const handleSelectRow = (id: string, selected: boolean) => { if (onSelectionChange) { onSelectionChange( selected ? [...selectedIds, id] : selectedIds.filter((selectedId) => selectedId !== id) ); } }; const getCellValue = (row: T, column: Column) => { if (typeof column.accessor === 'function') { return column.accessor(row); } return row[column.accessor] as React.ReactNode; }; return (
{onSelectionChange && ( )} {columns.map((column) => ( ))} {isLoading ? ( ) : data.length === 0 ? ( ) : ( data.map((row) => ( onRowClick?.(row)} > {onSelectionChange && ( )} {columns.map((column) => ( ))} )) )}
column.sortable && onSort?.(column.id)} >
{column.header} {column.sortable && sortField === column.id && ( )}
Loading...
{emptyMessage}
e.stopPropagation()} > handleSelectRow(row.id, e.target.checked)} aria-label={`Select row ${row.id}`} /> {getCellValue(row, column)}
); } DataTable.displayName = 'DataTable'; ``` ## Best Practices ### 1. Keep Business Logic Contained ```typescript // GOOD: Organism manages its own related state const ProductCard = ({ product, onAddToCart }) => { const [isAdding, setIsAdding] = useState(false); const handleAddToCart = async () => { setIsAdding(true); await onAddToCart(product.id); setIsAdding(false); }; return ; }; // BAD: State managed externally for no reason const ProductCard = ({ product, isAdding, onAddToCart }) => { return ; }; ``` ### 2. Accept Data Objects ```typescript // GOOD: Accept domain objects interface HeaderProps { user: User; navigation: NavLink[]; } // BAD: Flat props explosion interface HeaderProps { userName: string; userEmail: string; userAvatar: string; navItem1Label: string; navItem1Href: string; // ...endless props } ``` ### 3. Compose with Clear Hierarchy ```typescript // GOOD: Clear molecule/atom composition const Header = () => (
{/* Atom */} {/* Molecule */} {/* Molecule */} {/* Molecule */}
); ``` ## Anti-Patterns to Avoid ### 1. Organisms Within Organisms ```typescript // BAD: Nesting organisms const Dashboard = () => (
{/* Organism */} {/* Organism */} {/* Another organism */}
); // This should be a template, not an organism! ``` ### 2. Too Generic Organisms ```typescript // BAD: Generic "Section" organism const Section = ({ children }) =>
{children}
; // GOOD: Specific, purposeful organisms const ProductSection = ({ products }) => { ... }; const CommentSection = ({ comments }) => { ... }; ``` ## When to Use This Skill - Building page sections like headers and footers - Creating complex interactive components - Assembling product or content cards - Building data display components - Creating form containers with validation ## Related Skills - `atomic-design-fundamentals` - Core methodology overview - `atomic-design-molecules` - Composing atoms into molecules - `atomic-design-templates` - Page layouts without content