Crear una galería de imágenes rápida con Claude Code
Implementa una galería responsive con Claude Code, React, srcset, lightbox, errores comunes y checklist.
Una galería también es una decisión de producto
Crear una galería de imágenes con Claude Code no consiste solo en pedir “hazlo bonito”. Una galería puede vender un producto, demostrar un caso de éxito, documentar un taller o ayudar a comparar capturas antes y después. Si el prompt solo pide un masonry vistoso, es fácil recibir un demo correcto pero frágil: imágenes pesadas, textos alt pobres, saltos de layout, lightbox que solo funciona con mouse y una ruta de conversión poco clara.
En esta guía vamos a tratar la galería como una mejora publicable. Primero daremos restricciones a Claude Code, después construiremos un componente React tipado, añadiremos srcset, sizes, carga diferida y lightbox, y finalmente revisaremos errores reales antes de publicar. Para completar el contexto, lee también procesamiento de imágenes con Claude Code, optimización de performance y accesibilidad. Como referencias externas, mantén abiertas la documentación oficial de Claude Code, MDN sobre imágenes responsive, MDN sobre Lazy loading y WCAG 2.2.
Mi regla práctica es fijar el contrato de datos antes que el estilo visual. Cada imagen debe tener id, categoría, ancho, alto y un texto alternativo con sentido. Con eso, cambiar después a next/image, Astro assets o un CDN de imágenes es una decisión de infraestructura, no una reescritura de la UI.
Prompt con límites claros
Este prompt se puede usar como punto de partida. Lo importante es pedir código real, estados fallidos y una explicación de riesgos.
Implementa una galería de imágenes en React.
El objetivo es una UI rápida para artículos, casos de éxito, capturas de producto y fotos de talleres.
Condiciones:
- No romper rutas existentes ni convenciones del design system.
- Definir un tipo de imagen con id, src, alt, width, height y category obligatorios.
- Usar CSS Grid para el layout responsive.
- Usar srcset, sizes, loading y fetchPriority de forma deliberada.
- Abrir un lightbox al hacer clic y cerrarlo con Escape.
- Cubrir arreglo vacío, error de carga de imagen, alt largo y móvil.
- Al final, explicar archivos modificados, pruebas y riesgos restantes.
Devuelve React/TypeScript y CSS listos para copiar, no pseudocódigo.
La diferencia se nota en la revisión. Claude Code deja de generar una maqueta grande y empieza a producir un cambio pequeño, verificable y alineado con el repositorio. Exigir width y height reduce cambios de layout. Exigir alt evita textos inútiles como “imagen 1”. Pedir riesgos obliga a una segunda pasada de revisión.
Arquitectura recomendada
Antes de escribir código, conviene mostrar el flujo completo. Así Claude Code separa mejor las responsabilidades.
flowchart LR
A["Imágenes originales"] --> B["Variantes por tamaño"]
B --> C["Array GalleryImage"]
C --> D["Filtro por categoría"]
D --> E["Tarjetas CSS Grid"]
E --> F["Lightbox"]
E --> G["Lighthouse y revisión manual"]
| Decisión | Valor inicial seguro | Revisar cuando |
|---|---|---|
| Layout | CSS Grid | Las alturas sean muy irregulares |
| Lazy loading | Solo bajo el primer viewport | La imagen principal tarde demasiado |
| Variantes | Aproximadamente 480/960/1440px | Haya muchos usuarios en pantallas grandes |
| Lightbox | Comportamiento accesible mínimo | La galería afecte compras o leads |
No hace falta empezar con una librería masonry. En muchas páginas basta con tarjetas estables, miniaturas rápidas y una vista ampliada clara. Menos dependencias también significa menos superficie para que Claude Code cambie archivos no relacionados.
Implementación React copiable
El siguiente componente es ligero a propósito. Funciona en Vite, puede moverse a un client component de Next.js y más adelante puede adaptarse al componente de imagen que ya use tu proyecto.
import { useEffect, useMemo, useState } from "react";
import "./image-gallery.css";
export type GalleryImage = {
id: string;
src: string;
alt: string;
width: number;
height: number;
category: string;
sources?: Array<{ width: number; src: string }>;
};
function buildSrcSet(image: GalleryImage) {
if (!image.sources?.length) return undefined;
return [...image.sources]
.sort((a, b) => a.width - b.width)
.map((source) => `${source.src} ${source.width}w`)
.join(", ");
}
export function ImageGallery({ images }: { images: GalleryImage[] }) {
const [category, setCategory] = useState("all");
const [activeId, setActiveId] = useState<string | null>(null);
const [brokenIds, setBrokenIds] = useState<Set<string>>(() => new Set());
const categories = useMemo(() => {
return ["all", ...Array.from(new Set(images.map((image) => image.category)))];
}, [images]);
const visibleImages = useMemo(() => {
if (category === "all") return images;
return images.filter((image) => image.category === category);
}, [category, images]);
const activeImage = visibleImages.find((image) => image.id === activeId);
useEffect(() => {
if (!activeImage) return;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") setActiveId(null);
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [activeImage]);
function markBroken(id: string) {
setBrokenIds((current) => new Set(current).add(id));
}
if (images.length === 0) {
return <p className="gallery-empty">No images are available yet.</p>;
}
return (
<section className="gallery" aria-label="Image gallery">
<div className="gallery-toolbar" aria-label="Filter images by category">
{categories.map((item) => (
<button
className={item === category ? "is-active" : ""}
key={item}
onClick={() => setCategory(item)}
type="button"
>
{item === "all" ? "All" : item}
</button>
))}
</div>
<div className="gallery-grid">
{visibleImages.map((image, index) => {
const isBroken = brokenIds.has(image.id);
return (
<button
className="gallery-card"
key={image.id}
onClick={() => setActiveId(image.id)}
type="button"
>
{isBroken ? (
<span className="gallery-fallback">Image unavailable</span>
) : (
<img
alt={image.alt}
width={image.width}
height={image.height}
src={image.src}
srcSet={buildSrcSet(image)}
sizes="(min-width: 960px) 33vw, (min-width: 640px) 50vw, 100vw"
loading={index < 2 ? "eager" : "lazy"}
fetchPriority={index === 0 ? "high" : "auto"}
style={{ aspectRatio: `${image.width} / ${image.height}` }}
onError={() => markBroken(image.id)}
/>
)}
<span>{image.alt}</span>
</button>
);
})}
</div>
{activeImage && (
<div
className="gallery-lightbox"
role="dialog"
aria-modal="true"
aria-label={activeImage.alt}
tabIndex={-1}
onClick={() => setActiveId(null)}
>
<button className="gallery-close" onClick={() => setActiveId(null)} type="button">
Close
</button>
<img
alt={activeImage.alt}
width={activeImage.width}
height={activeImage.height}
src={activeImage.src}
onClick={(event) => event.stopPropagation()}
/>
</div>
)}
</section>
);
}
.gallery {
display: grid;
gap: 1rem;
}
.gallery-toolbar {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.gallery-toolbar button,
.gallery-card,
.gallery-close {
border: 1px solid #d4d4d8;
background: #ffffff;
color: #18181b;
cursor: pointer;
}
.gallery-toolbar button {
border-radius: 999px;
padding: 0.45rem 0.8rem;
}
.gallery-toolbar .is-active {
background: #18181b;
color: #ffffff;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1rem;
}
.gallery-card {
display: grid;
gap: 0.5rem;
padding: 0;
overflow: hidden;
border-radius: 8px;
text-align: left;
}
.gallery-card img {
width: 100%;
object-fit: cover;
background: #f4f4f5;
}
.gallery-fallback {
display: grid;
min-height: 180px;
place-items: center;
background: #f4f4f5;
color: #71717a;
}
.gallery-card span {
padding: 0 0.75rem 0.75rem;
font-size: 0.875rem;
}
.gallery-lightbox {
position: fixed;
inset: 0;
z-index: 50;
display: grid;
place-items: center;
padding: 2rem;
background: rgb(0 0 0 / 0.86);
}
.gallery-lightbox img {
max-width: min(100%, 1100px);
max-height: 82vh;
object-fit: contain;
}
.gallery-close {
position: absolute;
top: 1rem;
right: 1rem;
border-radius: 6px;
padding: 0.5rem 0.75rem;
}
.gallery-empty {
color: #71717a;
}
En un proyecto real puedes cambiar el img por la abstracción existente. Lo importante es no perder alt, width, height y sizes. Pide a Claude Code que revise si fetchPriority encaja con tus navegadores objetivo.
Datos y casos de uso
Mantén los datos fuera del componente. Aunque vengan de un CMS, normalízalos antes de renderizar.
import type { GalleryImage } from "./ImageGallery";
export const galleryImages: GalleryImage[] = [
{
id: "case-study-dashboard",
src: "/images/gallery/dashboard-960.webp",
alt: "Analytics dashboard after Claude Code refactoring",
width: 960,
height: 640,
category: "Case study",
sources: [
{ width: 480, src: "/images/gallery/dashboard-480.webp" },
{ width: 960, src: "/images/gallery/dashboard-960.webp" },
{ width: 1440, src: "/images/gallery/dashboard-1440.webp" },
],
},
{
id: "workshop-room",
src: "/images/gallery/workshop-960.webp",
alt: "Team workshop board with Claude Code review checklist",
width: 960,
height: 720,
category: "Training",
},
{
id: "product-shot",
src: "/images/gallery/template-pack-960.webp",
alt: "Claude Code template pack product preview",
width: 960,
height: 540,
category: "Product",
},
];
Primer caso: portfolio o página de caso de éxito. La imagen debe ayudar a comparar resultados y llevar a una historia, formulario o consulta.
Segundo caso: ecommerce o producto digital. Capturas, ejemplos de uso, comparativas y vistas posteriores a la compra reducen dudas, pero no conviene cargar todas las imágenes grandes antes del CTA.
Tercer caso: formación, eventos o documentación interna. Pizarras, capturas paso a paso, antes/después y pantallas de error se vuelven material reutilizable. En entornos internos hay que revisar nombres de clientes, correos y secretos.
Cuarto caso: artículos técnicos. Si hay varios bloques de código, una galería con diagramas y capturas de verificación ayuda a no perder el hilo.
Errores comunes
El primer error es aplicar lazy loading a la imagen visible al cargar la página. Esa imagen puede afectar LCP, así que el primer elemento suele necesitar eager y quizá fetchPriority="high". El error opuesto es cargar todo de inmediato.
El segundo error es omitir width y height. Cada imagen mueve la tarjeta al cargarse y la página parece inestable. Pide a Claude Code una revisión explícita de CLS.
El tercer error es escribir alt como si fuera una lista de palabras clave SEO. El texto alternativo debe sustituir a la imagen cuando no se puede ver.
El cuarto error es un lightbox solo para mouse. Debe tener botón de cierre con nombre, Escape, foco visible y comportamiento aceptable en móvil. Si necesitas focus trap estricto, evalúa Radix UI o React Aria.
El quinto error es no definir reglas de operación. Una sola imagen PNG de 6 MB subida al CMS puede arruinar la página. Define tamaño máximo, formatos permitidos, nombres de archivo y revisión en CLAUDE.md.
Verificación antes de publicar
Prueba filtros, lightbox, teclado, datos vacíos, imágenes rotas y ancho de 375px. Con Playwright puedes empezar así:
import { expect, test } from "@playwright/test";
test("image gallery filters and opens a lightbox", async ({ page }) => {
await page.goto("/gallery");
await expect(page.getByRole("region", { name: "Image gallery" })).toBeVisible();
await page.getByRole("button", { name: "Training" }).click();
await expect(page.getByRole("button", { name: /workshop/i })).toBeVisible();
await page.getByRole("button", { name: /workshop/i }).click();
await expect(page.getByRole("dialog")).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.getByRole("dialog")).toBeHidden();
});
Para la revisión, no preguntes “¿se ve bien?”. Fija criterios: número de peticiones iniciales, coherencia de srcset y sizes, calidad del alt, roles de botones o enlaces, overflow en móvil, datos rotos del CMS y posible exposición de información privada.
CTA y resultado práctico
Una galería debe apoyar una ruta de negocio. La imagen de un caso debe llevar al caso completo, una imagen de producto a detalles de compra, y una foto de taller a formación y consultoría Claude Code. Para profundizar, continúa con lazy loading de imágenes y desarrollo React con Claude Code.
Al probar este flujo, separar contrato de datos, componente, CSS y revisión hizo que el cambio fuera mucho más fácil de auditar. La obligación de incluir width, height y alt encontró datos débiles antes del despliegue. La revisión final usó DevTools Network, Lighthouse y una prueba manual en móvil.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Workflow de Obsidian a CLAUDE.md con Claude Code
Convierte notas de trabajo de Obsidian en notas operativas de CLAUDE.md para no repetir contexto.
Claude Code Revenue CTA Routing: de artículos a PDF, Gumroad y consulta
Un flujo con Claude Code para dirigir lectores a PDF gratis, Gumroad o consulta según intención.
Reglas de handoff para equipos con Claude Code: evidencia, permisos, rollback e ingresos
Formato práctico para entregar trabajo de Claude Code con pruebas, permisos, rollback, PDF gratis, Gumroad y consulta.