🎯 Objetivo del tema
El administrador aún no ha insertado un video para esta sección.
🗺️ Mapa del tema
Cuando tu app crece de 5 componentes a 50, surge un problema: el Button de la pantalla de login se ve diferente al de la pantalla de perfil, y nadie sabe por qué. El Atomic Design (de Brad Frost) resuelve esto con una jerarquía clara: átomos (Button, Input, Label) → moléculas (SearchBar = Input + Button) → organismos (Header con logo + nav + search) → templates (layout principal) → páginas (instancias concretas). Es la metodología que usan Linear, Notion, Figma, y los designers profesionales. Hoy la aplicas a tu app de Atlas Mundial.
De átomos a páginas: la pirámide de complejidad.
Los componentes más pequeños, sin estado propio.
Combinaciones de átomos que forman unidades funcionales.
Secciones completas con su propia lógica y estado.
La estructura reutilizable y las páginas concretas.
Tu catálogo vivo de componentes: cómo se ven todas las variantes.
3.6.1 Atomic Design: la metodología que escaló React
Brad Frost publicó Atomic Design en 2013 inspirado en la química: así como la materia se compone de átomos, las interfaces se componen de 5 niveles de componentes. Es la metodología estándar en design systems profesionales: Linear, Notion, Figma, IBM, Salesforce. Te da un vocabulario común y una jerarquía clara que escala de 5 a 5000 componentes sin perder consistencia.
| Nivel | Qué es | Ejemplos en Atlas Mundial | Estado propio |
|---|---|---|---|
| Átomos | Componentes más pequeños, sin estado. | Button, Input, Label, Badge, Spinner, Avatar. | No. |
| Moléculas | Combinación de átomos que forman una unidad. | SearchBar (Input + Button), FormField (Label + Input + error). | A veces (input del usuario). |
| Organismos | Sección completa con su propia lógica. | Header (logo + nav + search), PaisCard (imagen + textos + acciones). | Sí (datos del API, hover state). |
| Templates | Layout reutilizable sin contenido. | Layout principal (Header + main + Footer). | No (recibe children). |
| Páginas | Instancia concreta con datos reales. | Página de Argentina (Header + PaisCard + Clima + Mapa). | Sí (datos específicos). |
El administrador aún no ha insertado un video para esta sección.
3.6.2 Átomos: Button, Input, Label, Badge, Spinner
Los átomos son los componentes MÁS PEQUEÑOS de tu app. No tienen estado propio (no son useState), no hacen fetch, no tienen lógica. Son BLOQUES VISUALES puros: Button, Input, Label, Badge, Spinner, Avatar, Icon. Son la base de todo lo demás, así que deben estar bien hechos y ser REUTILIZABLES. Un Button con 10 props para 10 variantes es un átomo bien diseñado.
El átomo Button con 4 variantes y 3 tamaños
// atoms/Button.jsx: el átomo más importante de tu app
import './Button.css';
// Props del Button: variant, size, disabled, loading, onClick, children
export function Button({
variant = 'primary', // primary | secondary | danger | ghost
size = 'md', // sm | md | lg
disabled = false,
loading = false,
onClick,
type = 'button',
children,
...rest
}) {
return (
<button
type={type}
onClick={onClick}
disabled={disabled || loading}
className={`btn btn-${variant} btn-${size} ${loading ? 'btn-loading' : ''}`}
{...rest}
>
{loading ? <span className="spinner" /> : children}
</button>
);
}
// Button.css: variantes con CSS o Tailwind
.btn { /* base */ padding: 0.5rem 1rem; border-radius: 8px; font-weight: 500; transition: all 0.2s; cursor: pointer; border: none; }
.btn-primary { background: var(--color-primario); color: white; }
.btn-primary:hover { background: var(--color-primario-hover); }
.btn-secondary { background: white; color: var(--color-texto); border: 1px solid var(--color-borde); }
.btn-danger { background: var(--color-error); color: white; }
.btn-ghost { background: transparent; color: var(--color-texto); }
.btn-sm { padding: 0.25rem 0.5rem; font-size: 0.875rem; }
.btn-md { padding: 0.5rem 1rem; font-size: 1rem; }
.btn-lg { padding: 0.75rem 1.5rem; font-size: 1.125rem; }
.btn-loading { opacity: 0.6; cursor: wait; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
// Uso: el mismo Button en TODA la app
<Button variant="primary" size="md">Guardar</Button>
<Button variant="danger" size="sm" onClick={eliminar}>Eliminar</Button>
<Button variant="secondary" disabled>Cancelar</Button>
<Button variant="primary" loading>Enviando...</Button>Átomo Button: el componente más reutilizado de tu app
El administrador aún no ha insertado un video para esta sección.
3.6.3 Moléculas: SearchBar, FormField, Card
Las moléculas son combinaciones de átomos que forman una UNIDAD funcional. SearchBar = Input + Button + (opcional) Icon. FormField = Label + Input + (opcional) error message. Card = Image + Title + Description + (opcional) actions. Las moléculas tienen su PROPIA identidad: cuando ves una SearchBar, sabes que es 'el buscador', no solo 'un input con un botón al lado'.
Molécula SearchBar: input + botón con debounce integrado
// molecules/SearchBar.jsx
import { useState } from 'react';
import { useDebounce } from '../hooks/useDebounce';
import { Input } from '../atoms/Input';
import { Button } from '../atoms/Button';
export function SearchBar({ placeholder = 'Buscar...', onSearch, delay = 300 }) {
const [termino, setTermino] = useState('');
const terminoDebounced = useDebounce(termino, delay);
// Cuando el debounced cambia, llama a onSearch
if (onSearch && terminoDebounced) onSearch(terminoDebounced);
return (
<form className="search-bar" onSubmit={e => e.preventDefault()}>
<Input
type="search"
placeholder={placeholder}
value={termino}
onChange={e => setTermino(e.target.value)}
/>
<Button type="submit" variant="primary" size="md" disabled={!termino}>
Buscar
</Button>
</form>
);
}
// Molécula FormField: label + input + error message
// molecules/FormField.jsx
import { Input } from '../atoms/Input';
export function FormField({ label, name, type = 'text', value, onChange, error, required }) {
return (
<div className={`form-field ${error ? 'has-error' : ''}`}>
<label htmlFor={name}>{label}{required && <span className="required">*</span>}</label>
<Input id={name} name={name} type={type} value={value} onChange={onChange} />
{error && <span className="error-message">{error}</span>}
</div>
);
}
// Uso:
<FormField
label="Correo electrónico"
name="email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
error={errors.email}
required
/>Moléculas: SearchBar y FormField combinando átomos
El administrador aún no ha insertado un video para esta sección.
3.6.4 Organismos: Header, UserCard, ProductList
Los organismos son SECCIONES COMPLETAS con su propia lógica y estado. Header (logo + nav + search + user menu) es un organismo: tiene estado (usuario autenticado o no, menú abierto o cerrado), combina moléculas (SearchBar) y átomos (Button, Avatar), y tiene una responsabilidad clara: la navegación principal. Los organismos son el corazón de tu app.
Organismo Header: nav + search + user menu
// organisms/Header.jsx
import { Link, NavLink } from 'react-router-dom';
import { useState } from 'react';
import { Button } from '../atoms/Button';
import { Avatar } from '../atoms/Avatar';
import { SearchBar } from '../molecules/SearchBar';
export function Header({ usuario, onLogout }) {
const [menuAbierto, setMenuAbierto] = useState(false);
return (
<header className="app-header">
<Link to="/" className="logo">🌍 Atlas Mundial</Link>
<nav className="nav-links">
<NavLink to="/" end>Inicio</NavLink>
<NavLink to="/paises">Países</NavLink>
<NavLink to="/mapa">Mapa</NavLink>
<NavLink to="/sobre">Sobre</NavLink>
</nav>
<SearchBar onSearch={(q) => console.log('Buscar:', q)} />
{usuario ? (
<div className="user-menu">
<button onClick={() => setMenuAbierto(!menuAbierto)}>
<Avatar src={usuario.avatar} alt={usuario.nombre} />
</button>
{menuAbierto && (
<div className="dropdown">
<Link to="/perfil">Mi perfil</Link>
<Link to="/favoritos">Favoritos</Link>
<button onClick={onLogout}>Cerrar sesión</button>
</div>
)}
</div>
) : (
<Button variant="primary" size="sm">Iniciar sesión</Button>
)}
</header>
);
}Organismo Header: nav + search + user menu con estado
El administrador aún no ha insertado un video para esta sección.
3.6.5 Templates y páginas: layout + instancias
Los templates son la ESTRUCTURA reutilizable sin contenido específico: layout principal con Header arriba, contenido en el medio, Footer abajo. Las páginas son las INSTANCIAS concretas del template con datos reales. Esta separación te permite cambiar todo el layout sin tocar las páginas, y cambiar una página sin tocar el layout. Es la separación de responsabilidades definitiva.
Template MainLayout: el esqueleto reutilizable
// templates/MainLayout.jsx: el template principal de tu app
import { Header } from '../organisms/Header';
import { Footer } from '../organisms/Footer';
import { Outlet } from 'react-router-dom';
export function MainLayout({ usuario, onLogout }) {
return (
<div className="app-layout">
<Header usuario={usuario} onLogout={onLogout} />
<main className="app-main">
{/* Outlet renderiza el componente de la ruta activa */}
<Outlet />
</main>
<Footer />
</div>
);
}
// pages/Home.jsx: una página concreta
import { useFetch } from '../hooks/useFetch';
import { SearchBar } from '../molecules/SearchBar';
import { PaisCard } from '../organisms/PaisCard';
export function Home() {
const { data, cargando } = useFetch('https://restcountries.com/v3.1/all?fields=name,capital,population,flag,region,cca3');
return (
<div className="home-page">
<h1>🌍 Explora el mundo</h1>
<SearchBar onSearch={(q) => console.log(q)} />
{cargando ? <p>Cargando...</p> : (
<div className="paises-grid">
{data?.slice(0, 20).map(pais => <PaisCard key={pais.cca3} pais={pais} />)}
</div>
)}
</div>
);
}
// App.jsx: usa MainLayout como template de TODAS las rutas
<Routes>
<Route element={<MainLayout usuario={usuario} onLogout={logout} />}>
<Route path="/" element={<Home />} />
<Route path="/pais/:id" element={<DetallePais />} />
<Route path="/sobre" element={<Sobre />} />
</Route>
</Routes>Template MainLayout y pages concretas con Outlet de React Router
El administrador aún no ha insertado un video para esta sección.
3.6.6 Documentación de componentes con Storybook
Storybook es la herramienta estándar para documentar y testear componentes en aislamiento. Creas 'stories' para cada componente y cada variante, y tienes un catálogo vivo donde puedes ver cómo se ve tu Button primary, secondary, danger, disabled, loading, sin tener que navegar por toda tu app. Es lo que usan los equipos profesionales para mantener su design system. Vamos a configurarlo en Atlas Mundial.
Configurar Storybook en Vite + React
// Instalar: npx sb init --builder=vite
// Crea automáticamente la carpeta .storybook/ y stories/Button.stories.jsx
// stories/Button.stories.jsx: la documentación del Button
import { Button } from '../src/atoms/Button';
export default {
title: 'Átomos/Button',
component: Button,
argTypes: {
variant: { control: 'select', options: ['primary', 'secondary', 'danger', 'ghost'] },
size: { control: 'select', options: ['sm', 'md', 'lg'] },
disabled: { control: 'boolean' },
loading: { control: 'boolean' },
},
};
// Cada 'export const' es una 'story': una variante del componente
export const Primary = { args: { variant: 'primary', children: 'Guardar' } };
export const Secondary = { args: { variant: 'secondary', children: 'Cancelar' } };
export const Danger = { args: { variant: 'danger', children: 'Eliminar' } };
export const Ghost = { args: { variant: 'ghost', children: 'Más info' } };
export const Small = { args: { size: 'sm', children: 'Pequeño' } };
export const Large = { args: { size: 'lg', children: 'Grande' } };
export const Disabled = { args: { disabled: true, children: 'Deshabilitado' } };
export const Loading = { args: { loading: true, children: 'Cargando' } };
// Para correrlo: npm run storybook
// Abre en http://localhost:6006 y verás TODAS las variantes de tu Button en un solo lugar.
// Puedes cambiar las props con los controles y ver el cambio en tiempo real.Storybook: catálogo vivo de componentes
El administrador aún no ha insertado un video para esta sección.
📚 Contenido ampliado
Material adicional, referencias externas verificadas y ejemplos extendidos.
🤖 AI Mission
Atomic Design es la metodología que usan los profesionales. No es opcional cuando escalas.
Misión: Toma tu app de Atlas Mundial y refactorízala con Atomic Design: identifica los átomos (Button, Input, etc.), moléculas (SearchBar, FormField, PaisCard), organismos (Header, MapaConDatos), templates (MainLayout), y páginas (Home, DetallePais). Configura Storybook para documentar tus componentes. Pídele a la IA que revise la jerarquía. Anota: ¿qué encontraste que era un átomo mal clasificado como organismo? ¿y viceversa?
Pasos sugeridos
- Parte del proyecto atlas-mundial del Tema 3.5.
- Crea la estructura de carpetas: src/atoms, src/molecules, src/organisms, src/templates, src/pages, src/hooks, src/utils.
- Mueve Button, Input, Label, Avatar, Spinner, Badge a atoms/. Asegúrate que NO tengan useState.
- Crea o mueve SearchBar, FormField, PaisCard, ClimaCard, PersonajeCard a molecules/.
- Crea o mueve Header, MapaConDatos, ListaPaisesSection a organisms/. Estos SÍ tienen useState.
- Crea templates/MainLayout.jsx con <Outlet /> de React Router.
- Crea stories/Button.stories.jsx con 8 variantes (Primary, Secondary, Danger, Ghost, Small, Large, Disabled, Loading).
- Crea stories para Input, Avatar, SearchBar, PaisCard, Header.
- Configura Storybook: npx sb init --builder=vite. Ejecuta npm run storybook.
- Refactoriza Home y DetallePais para usar SOLO componentes de atoms/molecules/organisms.
- Pídele a la IA: 'Tengo esta app refactorizada con Atomic Design. Audita la jerarquía: ¿hay átomos que deberían ser moléculas? ¿organismos que deberían ser átomos? Dame 3 sugerencias concretas.'
- Aplica las 3 correcciones.
- Anota en tu cuaderno: 3 cosas que descubriste al refactorizar (componentes mal clasificados, duplicación que no veías, etc.).
📓 Entregable: Capturas de Storybook mostrando todos los componentes y sus variantes, estructura de carpetas final con atoms/molecules/organisms, código de MainLayout, y media página de cuaderno con tu auditoría de la jerarquía.
🚫 Errores típicos de razonamiento
Por qué: Los átomos son BLOQUES VISUALES puros. Si tienen useState, dejan de ser reutilizables: cada lugar que los usa tendría su propio estado. Es el error de mezclar 'presentational' (átomos) con 'container' (organismos). Si tu Input tiene useState, no es un átomo: es un organismo. La regla: átomos = solo props, organismos = useState y fetch. Si dudas, pregunta: '¿puedo usar este componente en 10 lugares sin que cada uno tenga su propio estado?' Si sí, es átomo.
Por qué: Si tienes ButtonPrimary, ButtonSecondary, ButtonDanger, ButtonGhost, ButtonSmall, ButtonLarge, etc., estás CREANDO componentes cuando podrías haber usado UN Button con props. Cada componente extra es código que mantener, testear, y documentar. Un Button con props variant y size es 12 props, 4 variantes, 3 tamaños: 12 combinaciones posibles. Es el ejemplo perfecto de por qué Atomic Design + props > componentes separados.
Por qué: Sin Storybook, los nuevos developers no saben qué componentes existen, qué props aceptan, o qué variantes tienen. Terminan creando ButtonNew porque no sabían que existía Button con variant='primary'. Storybook es la solución: catálogo vivo, búsqueda de componentes, controles interactivos. Para apps de 20+ componentes, vale la inversión de 1-2 días configurarlo. La alternativa barata: un archivo README.md con screenshots y tablas de props, pero Storybook es 10x mejor.
Por qué: Si tienes .card en 5 archivos .module.css, y cambias el border-radius en uno, los otros 4 quedan con el viejo. La solución: (1) usar custom properties de CSS (--radio-md) que cambias UNA vez. (2) Usar un componente compartido en vez de clases compartidas. (3) Si la duplicación es inevitable, acepta que es un trade-off entre DRY y mantenibilidad. La regla: duplicar código es OK; duplicar estilos sin razón es un problema.
Por qué: Una molécula que hace fetch deja de ser presentacional: depende de una API, tiene estado de loading/error, y no es reutilizable en otros contextos. SearchBar es molécula: solo recibe props y emite eventos. PaisCard que hace fetch a la API para obtener detalles es organismo: tiene estado, depende de la API, y tiene su propia responsabilidad. La regla: si el componente tiene useEffect, useState de datos del API, o maneja errores de fetch, es organismo. Si solo recibe props y emite eventos, es molécula o átomo.
🧪 Laboratorio práctico
Un design system no es documentación: es código que se ejecuta.
Laboratorio: Atlas Mundial refactorizado con Atomic Design + Storybook
Objetivo: Refactorizar la app de Atlas Mundial del Tema 3.5 con la metodología Atomic Design: atoms/, molecules/, organisms/, templates/, pages/. Configurar Storybook para documentar todos los componentes. El resultado: una arquitectura escalable, mantenible, y documentada.
Pasos
- Crea estructura: mkdir src/atoms src/molecules src/organisms src/templates src/pages src/hooks src/utils src/stories.
- Mueve Button, Input, Label, Avatar, Spinner, Badge a atoms/. Cada uno solo con props, sin useState.
- Crea o mueve SearchBar, FormField, PaisCard, ClimaCard, PersonajeCard a molecules/.
- Crea o mueve Header, Footer, MapaConClima, ListaPaises a organisms/. Estos SÍ tienen useState y fetch.
- Crea templates/MainLayout.jsx con <Outlet />.
- Refactoriza pages/Home.jsx y pages/DetallePais.jsx para que SOLO usen componentes de atoms/molecules/organisms/templates.
- Instala Storybook: npx sb init --builder=vite. Acepta la configuración por defecto.
- Crea stories/Button.stories.jsx con 8 variantes. Crea stories para Input, Avatar, SearchBar, PaisCard.
- Ejecuta: npm run storybook. Abre http://localhost:6006 y verifica que TODOS los componentes y sus variantes se ven.
- Ajusta las props con los controles de Storybook. Verifica que cada variante se ve bien.
- Verifica accesibilidad: cada componente tiene su label, role, focus visible.
- Haz commit: 'refactor: estructura Atomic Design + Storybook configurado'.
📓 Entregable: Capturas de Storybook con todos los componentes y variantes, código de atoms/molecules/organisms/templates/pages, y commit en Git.