Use Cases (Actualizado: 2/6/2026)

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.

Crear una galería de imágenes rápida con Claude Code

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ónValor inicial seguroRevisar cuando
LayoutCSS GridLas alturas sean muy irregulares
Lazy loadingSolo bajo el primer viewportLa imagen principal tarde demasiado
VariantesAproximadamente 480/960/1440pxHaya muchos usuarios en pantallas grandes
LightboxComportamiento accesible mínimoLa 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.

#Claude Code #galería de imágenes #React #responsive #performance
Gratis

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.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.