Consejos de Tailwind CSS con Claude Code: guía práctica para UI estable
Tailwind CSS con Claude Code: tokens, responsive, dark mode, componentes, safelist y pruebas visuales con ejemplos React.
Tailwind CSS permite construir interfaces con clases pequeñas como p-4, grid, text-sm o rounded-lg. Claude Code acelera ese trabajo porque puede leer el repositorio, editar archivos, ejecutar comandos y revisar cambios. La parte delicada es que una mejora rápida también puede dejar className enormes, colores inconsistentes, bugs solo en móvil, dark mode incompleto o clases dinámicas que desaparecen del CSS de producción.
Esta guía explica un flujo práctico para usar Claude Code con Tailwind CSS sin convertir la UI en una colección de parches. Cubrimos design tokens, utilidades responsive, extracción de componentes, cómo evitar class soup, dark mode, formularios, botones, cards, safelist, content scanning y verificaciones visuales con Playwright. Los ejemplos son de React + TypeScript, pero el criterio sirve para Astro, Next.js, Remix, Vite y otros stacks.
Las referencias oficiales son Tailwind Theme variables, Responsive design, Dark mode, Detecting classes in source files y Adding custom styles. Para React con tipos, consulta la guía de TypeScript de React. Para Claude Code, usa la documentación oficial. Para comparación visual, la página clave es Playwright Visual comparisons.
Si tus prompts aún son demasiado generales, empieza por 5 consejos para mejores prompts. Si el objetivo incluye experiencia móvil completa, conecta este flujo con la guía de PWA.
Empieza con una auditoría
No pidas a Claude Code que edite de inmediato. En Tailwind, los problemas suelen venir de decisiones pequeñas repartidas por la app: colores, espaciado, breakpoints, estados de formulario, dark mode, CTAs, anuncios y bloques de código. Primero pide un informe.
Revisa el uso de Tailwind CSS en este repositorio. No edites archivos todavía.
Entrega un informe con:
- Tokens existentes de color, spacing, radius, shadow y typography
- Componentes React con className demasiado largos
- Layouts que pueden romperse en 375px, 768px o 1440px
- Cobertura de light/dark mode
- Estilos repetidos en forms, buttons, cards y badges
- Clases Tailwind dinámicas que pueden faltar en CSS de producción
- Checks visuales existentes: Playwright, Storybook o navegador manual
En ClaudeCodeLab, Masa rompió una vez el espacio de un CTA móvil al ajustar cards mirando solo el desktop. La lección fue clara: una clase de Tailwind parece local, pero puede afectar lectura, anuncios y conversión.
Define design tokens antes de diseñar
Un design token es un valor de diseño con nombre: color, espaciado, fuente, radio, sombra o breakpoint. Tailwind CSS v4 usa @theme, que genera utilidades como bg-brand-600 o rounded-card. En proyectos antiguos con tailwind.config.ts, puedes aplicar el mismo criterio en theme.extend.
/* src/styles/app.css */
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--font-sans: Inter, system-ui, sans-serif;
--color-brand-50: #eef6ff;
--color-brand-100: #d9ebff;
--color-brand-600: #2563eb;
--color-brand-700: #1d4ed8;
--color-ink: #111827;
--color-muted: #6b7280;
--color-surface: #ffffff;
--color-danger: #dc2626;
--radius-card: 0.75rem;
--shadow-card: 0 16px 40px rgb(15 23 42 / 0.08);
}
Un buen prompt sería: “Usa los tokens existentes brand, surface y danger; agrega variables @theme solo si el valor se reutiliza.” Es más seguro que pedir “un azul más bonito”, porque eso termina mezclando blue, sky e indigo.
Trabaja responsive desde mobile-first
Tailwind es mobile-first: la clase base aplica a pantallas pequeñas, y sm:, md: o lg: agregan diferencias para pantallas mayores. Si empiezas por desktop y corriges móvil después, el grid, las imágenes y los CTAs suelen quedar inestables.
Mejora el product grid con Tailwind CSS.
Condiciones:
- 1 columna en 375px, 2 columnas desde 640px, 3 columnas desde 1024px
- Las imágenes mantienen proporción cuadrada
- Las cards tienen altura consistente
- El CTA queda alineado abajo
- No cambies el tipo de props Product
- Revisa screenshots móvil y desktop después
type Product = {
id: string;
name: string;
price: number;
imageUrl: string;
};
export function ProductGrid({ products }: { products: Product[] }) {
return (
<section className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{products.map((product) => (
<article
key={product.id}
className="flex h-full flex-col overflow-hidden rounded-card border border-slate-200 bg-surface shadow-card dark:border-slate-800 dark:bg-slate-950"
>
<img
src={product.imageUrl}
alt={product.name}
className="aspect-square w-full object-cover"
/>
<div className="flex flex-1 flex-col p-4">
<h3 className="line-clamp-2 text-base font-semibold text-ink dark:text-white">
{product.name}
</h3>
<p className="mt-2 text-sm text-muted dark:text-slate-400">
{product.price.toLocaleString("es-ES", {
style: "currency",
currency: "EUR",
})}
</p>
<button className="mt-auto rounded-lg bg-brand-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-brand-700 focus:outline-none focus:ring-2 focus:ring-brand-600 focus:ring-offset-2 dark:focus:ring-offset-slate-950">
Ver detalles
</button>
</div>
</article>
))}
</section>
);
}
aspect-square estabiliza la imagen, flex h-full flex-col estabiliza la card y mt-auto empuja el botón hacia abajo.
Extrae componentes antes de tener class soup
Class soup es un className tan largo que ya no se distingue base, variante y parche. No hace falta ocultar todo con @apply; normalmente basta con extraer botones, cards, badges e inputs repetidos.
import type { ButtonHTMLAttributes, ReactNode } from "react";
type ButtonVariant = "primary" | "secondary" | "danger";
const buttonVariants: Record<ButtonVariant, string> = {
primary: "bg-brand-600 text-white hover:bg-brand-700 focus:ring-brand-600",
secondary:
"border border-slate-300 bg-white text-slate-900 hover:bg-slate-50 focus:ring-slate-400 dark:border-slate-700 dark:bg-slate-900 dark:text-white",
danger: "bg-danger text-white hover:bg-red-700 focus:ring-danger",
};
function cn(...classes: Array<string | false | null | undefined>) {
return classes.filter(Boolean).join(" ");
}
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: ButtonVariant;
loading?: boolean;
children: ReactNode;
};
export function Button({
variant = "primary",
loading = false,
disabled,
className,
children,
...props
}: ButtonProps) {
return (
<button
className={cn(
"inline-flex min-h-10 items-center justify-center rounded-lg px-4 py-2 text-sm font-semibold transition focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 dark:focus:ring-offset-slate-950",
buttonVariants[variant],
className,
)}
disabled={disabled || loading}
{...props}
>
{loading ? "Procesando..." : children}
</button>
);
}
Pide a Claude Code que mantenga las clases como strings completos. Evita bg-${color}-600, porque Tailwind puede no detectarlo al generar CSS.
Dark mode, formularios y CTA van juntos
Dark mode no es solo cambiar el fondo. Hay que revisar texto, border, shadow, input, error, disabled y focus ring. En un formulario, el estado de error importa tanto como el estado exitoso.
"use client";
import type { FormEvent } from "react";
type LeadFormProps = {
onSubmit: (values: { email: string; message: string }) => void;
error?: string;
};
export function LeadForm({ onSubmit, error }: LeadFormProps) {
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
onSubmit({
email: String(formData.get("email") ?? ""),
message: String(formData.get("message") ?? ""),
});
}
return (
<form
onSubmit={handleSubmit}
className="space-y-4 rounded-card border border-slate-200 bg-white p-5 shadow-card dark:border-slate-800 dark:bg-slate-950"
>
<label className="block text-sm font-medium text-slate-900 dark:text-white">
Email
<input
name="email"
type="email"
required
aria-describedby={error ? "lead-form-error" : undefined}
className="mt-1 w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 outline-none focus:border-brand-600 focus:ring-2 focus:ring-brand-600/20 dark:border-slate-700 dark:bg-slate-900 dark:text-white"
placeholder="you@example.com"
/>
</label>
{error ? (
<p id="lead-form-error" className="text-sm font-medium text-danger">
{error}
</p>
) : null}
<button className="w-full rounded-lg bg-brand-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-brand-700 focus:outline-none focus:ring-2 focus:ring-brand-600 focus:ring-offset-2 dark:focus:ring-offset-slate-950">
Solicitar consultoría
</button>
</form>
);
}
Para revisar accesibilidad, enlaza también la guía de accesibilidad con Claude Code.
Safelist y content scanning
Tailwind genera CSS a partir de las clases que detecta en los archivos fuente. Si construyes clases dinámicamente, puede que no aparezcan en producción. Usa mapas estáticos.
type Status = "success" | "warning" | "danger";
const statusClasses: Record<Status, string> = {
success: "bg-emerald-50 text-emerald-700 ring-emerald-600/20",
warning: "bg-amber-50 text-amber-800 ring-amber-600/20",
danger: "bg-red-50 text-red-700 ring-red-600/20",
};
export function StatusBadge({ status, label }: { status: Status; label: string }) {
return (
<span
className={`inline-flex items-center rounded-full px-2.5 py-1 text-xs font-semibold ring-1 ring-inset ${statusClasses[status]}`}
>
{label}
</span>
);
}
Si las clases vienen de un paquete externo o de un CMS, usa @source o @source inline() con moderación.
@import "tailwindcss";
@source "../node_modules/@acme/ui-kit";
@source inline("bg-emerald-50");
@source inline("text-emerald-700");
@source inline("bg-amber-50");
@source inline("text-amber-800");
Verifica visualmente
Un build correcto no demuestra que la UI esté bien. Si tienes Playwright, captura las vistas principales.
import { expect, test } from "@playwright/test";
const viewports = [
{ name: "mobile", size: { width: 375, height: 812 } },
{ name: "tablet", size: { width: 768, height: 1024 } },
{ name: "desktop", size: { width: 1440, height: 960 } },
];
for (const viewport of viewports) {
test(`pricing page visual check - ${viewport.name}`, async ({ page }) => {
await page.setViewportSize(viewport.size);
await page.goto("/pricing");
await expect(page.getByRole("main")).toHaveScreenshot(
`pricing-${viewport.name}.png`,
{ maxDiffPixelRatio: 0.01 },
);
});
}
Al final, pide a Claude Code una revisión con archivos cambiados, riesgos, viewports comprobados, dark mode y pasos manuales si Playwright no existe.
Casos de uso y errores comunes
| Caso de uso | Pedido a Claude Code | Foco Tailwind |
|---|---|---|
| Landing page | Revisar hero, CTA, precios y testimonios juntos | Espaciado, jerarquía, CTA |
| Dashboard SaaS | Ordenar tablas, filtros, sidebar y estados vacíos | Densidad, overflow, sticky header |
| Formulario | Cubrir input, error, success, loading, disabled | Focus ring, labels, targets móviles |
| Sitio de contenido | Revisar texto, código, anuncios y CTA | Ancho de lectura, scroll de código, enlaces |
Los errores habituales son clases dinámicas, revisar solo desktop, dark mode incompleto, abusar de @apply, añadir colores casi iguales, olvidar errores de formulario y no hacer screenshots.
CTA y resultado probado
Las mejoras de Tailwind deben apoyar el negocio: CTA al final del artículo, pricing card de un producto, formulario de consulta o descarga de un recurso. ClaudeCodeLab ofrece una cheatsheet gratuita de Claude Code, productos y plantillas y formación o consultoría para equipos.
Al probar este flujo en páginas de ClaudeCodeLab, lo que más redujo retrabajo fue la auditoría inicial y el screenshot de 375px. Detectaron problemas de espaciado en CTA, focus de formularios y contraste en dark mode antes de que se convirtieran en otra limpieza de className.
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
Escalera de permisos de Claude Code para ampliar acceso sin perder control
Pasa de read-only a ediciones limitadas, comandos de prueba y checks de deploy con menos riesgo.
Claude Code Small PR Proof Pack: cambios pequeños que sí se pueden revisar
Un paquete de prueba para PRs de Claude Code: diff, checks, URL pública, CTA y rollback.
Gate de revisión antes del commit con Claude Code
Cómo revisar con Claude Code antes del commit: diff, build, URL pública, Gumroad, consultoría, tests y archivos ajenos.