Patrón CSS Architecture
Cómo organizar CSS con utility-first classes y component-scoped layers. Cubre Tailwind CSS, CSS layers, BEM, CSS modules, y design tokens.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Overview
CSS architecture es sobre organizar styles para que escalen sin convertirse en un maintenance nightmare. El utility-first con component-scoped layers pattern combina Tailwind CSS (o similar utility frameworks) para rapid layout y spacing con component-scoped CSS para complex, reusable components. CSS @layer provee cascade control — las utilities pueden overridear component styles, y component styles pueden overridear base styles, todo sin !important. Este approach te da la speed de utility classes para common patterns y la maintainability de scoped styles para complex components.
When to Use
- Aplicaciones con 50+ components que necesitan consistent styling
- Teams donde múltiples developers escriben CSS simultáneamente
- Design systems que necesitan escalar across múltiples projects
- Projects usando Tailwind CSS, UnoCSS, o similar utility-first frameworks
- Cuando necesitás cascade control — base styles < component styles < utility overrides
When NOT to Use
- Simple landing pages con menos de 10 components
- Projects donde CSS ya está well-organized con BEM u otra methodology
- Legacy projects donde introducir un new CSS architecture sería costly
- Static sites sin reusable components
Solution
CSS layers para cascade control
/* styles.css — CSS cascade layers */
@layer base, components, utilities;
/* Base layer — resets, typography, element defaults */
@layer base {
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-sans);
line-height: 1.6;
color: var(--color-text);
background-color: var(--color-bg);
}
h1, h2, h3, h4, h5, h6 {
font-weight: 700;
line-height: 1.2;
}
a {
color: var(--color-primary);
text-decoration: none;
}
}
/* Components layer — reusable component styles */
@layer components {
.btn {
display: inline-flex;
align-items: center;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
transition: all 0.15s;
}
.btn-primary {
background-color: var(--color-primary);
color: white;
}
.btn-primary:hover {
background-color: var(--color-primary-dark);
}
.card {
background-color: white;
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
}
/* Utilities layer — single-purpose helpers (highest priority) */
@layer utilities {
.text-center { text-align: center; }
.text-right { text-align: right; }
.hidden { display: none; }
.mt-4 { margin-top: 1rem; }
.mb-4 { margin-bottom: 1rem; }
.w-full { width: 100%; }
}
Tailwind CSS v4 configuration
/* app.css — Tailwind CSS v4 CSS-first configuration */
@import "tailwindcss";
@theme {
--color-brand-50: #eff6ff;
--color-brand-100: #dbeafe;
--color-brand-200: #bfdbfe;
--color-brand-300: #93c5fd;
--color-brand-400: #60a5fa;
--color-brand-500: #3b82f6;
--color-brand-600: #2563eb;
--color-brand-700: #1d4ed8;
--color-brand-800: #1e40af;
--color-brand-900: #1e3a8a;
--font-sans: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", monospace;
--radius-card: 0.5rem;
--radius-button: 0.375rem;
--shadow-card: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-card-hover: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* Component classes usando @apply */
@layer components {
.btn {
@apply inline-flex items-center px-4 py-2 rounded-button font-medium transition-all;
}
.btn-primary {
@apply bg-brand-500 text-white hover:bg-brand-600;
}
.btn-secondary {
@apply bg-white text-slate-700 border border-slate-300 hover:bg-slate-50;
}
.btn-danger {
@apply bg-red-500 text-white hover:bg-red-600;
}
.card {
@apply bg-white border border-slate-200 rounded-card p-6 shadow-card;
}
.card-hover {
@apply hover:shadow-card-hover hover:border-brand-400 transition-all;
}
.input {
@apply w-full px-3 py-2 border border-slate-300 rounded-button
focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent;
}
.badge {
@apply inline-flex items-center px-2 py-0.5 text-xs font-medium rounded;
}
.badge-brand {
@apply bg-brand-100 text-brand-700;
}
}
CSS modules para component-scoped styles
/* Button.module.css — scoped component styles */
.button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: var(--radius-button);
font-weight: 500;
transition: all 0.15s ease;
border: none;
cursor: pointer;
}
.primary {
background-color: var(--color-brand-500);
color: white;
}
.primary:hover {
background-color: var(--color-brand-600);
}
.secondary {
background-color: white;
color: var(--color-slate-700);
border: 1px solid var(--color-slate-300);
}
.secondary:hover {
background-color: var(--color-slate-50);
}
.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.icon {
width: 1rem;
height: 1rem;
}
// Button.jsx — usando CSS module
import styles from './Button.module.css';
function Button({ variant = 'primary', disabled, icon: Icon, children, ...props }) {
const className = [
styles.button,
styles[variant],
disabled && styles.disabled,
].filter(Boolean).join(' ');
return (
<button className={className} disabled={disabled} {...props}>
{Icon && <Icon className={styles.icon} />}
{children}
</button>
);
}
export default Button;
BEM methodology para complex components
/* search-widget.css — BEM naming convention */
.search-widget {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem;
background: white;
border: 1px solid var(--color-border);
border-radius: var(--radius-card);
}
.search-widget__input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-button);
font-size: 0.875rem;
}
.search-widget__input--focused {
border-color: var(--color-brand-500);
box-shadow: 0 0 0 2px var(--color-brand-100);
}
.search-widget__input--error {
border-color: var(--color-red-500);
}
.search-widget__suggestions {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid var(--color-border);
border-radius: var(--radius-button);
max-height: 200px;
overflow-y: auto;
z-index: 10;
}
.search-widget__suggestion {
padding: 0.5rem 0.75rem;
cursor: pointer;
}
.search-widget__suggestion:hover,
.search-widget__suggestion--active {
background-color: var(--color-brand-50);
}
.search-widget__clear {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
color: var(--color-slate-400);
}
Design tokens como CSS custom properties
/* tokens.css — design tokens */
:root {
/* Colors — Brand */
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-600: #2563eb;
--color-brand-700: #1d4ed8;
/* Colors — Neutral */
--color-slate-50: #f8fafc;
--color-slate-100: #f1f5f9;
--color-slate-200: #e2e8f0;
--color-slate-300: #cbd5e1;
--color-slate-600: #475569;
--color-slate-700: #334155;
--color-slate-900: #0f172a;
/* Colors — Semantic */
--color-bg: var(--color-slate-50);
--color-text: var(--color-slate-900);
--color-text-muted: var(--color-slate-600);
--color-border: var(--color-slate-200);
--color-primary: var(--color-brand-500);
--color-primary-dark: var(--color-brand-600);
/* Typography */
--font-sans: "Inter", system-ui, -apple-system, sans-serif;
--font-mono: "JetBrains Mono", "Fira Code", monospace;
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
--font-size-2xl: 1.5rem;
/* Spacing */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
/* Radius */
--radius-sm: 0.25rem;
--radius-button: 0.375rem;
--radius-card: 0.5rem;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-card: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.15);
/* Transitions */
--transition-fast: 150ms ease;
--transition-base: 200ms ease;
--transition-slow: 300ms ease;
}
/* Dark mode tokens */
@media (prefers-color-scheme: dark) {
:root {
--color-bg: var(--color-slate-900);
--color-text: #f1f5f9;
--color-text-muted: var(--color-slate-600);
--color-border: #1e293b;
}
}
Combinando utility-first con component classes
// ProductCard.jsx — utility classes para layout, component class para complex styles
function ProductCard({ product }) {
return (
<div className="card card-hover p-6 flex flex-col gap-4">
<img
src={product.image}
alt={product.name}
className="w-full h-48 object-cover rounded-card"
/>
<div className="flex flex-col gap-2">
<h3 className="text-lg font-bold text-slate-900">{product.name}</h3>
<p className="text-sm text-slate-600">{product.description}</p>
</div>
<div className="flex items-center justify-between mt-auto">
<span className="text-2xl font-bold text-brand-600">
${product.price}
</span>
<button className="btn btn-primary">
Add to Cart
</button>
</div>
</div>
);
}
Responsive utility patterns
/* responsive.css — responsive utilities con container queries */
@layer utilities {
/* Flex utilities */
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.gap-2 { gap: 0.5rem; }
.gap-4 { gap: 1rem; }
/* Grid utilities */
.grid { display: grid; }
.grid-cols-1 { grid-template-columns: repeat(1, 1fr); }
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
/* Responsive — media queries */
@media (min-width: 768px) {
.md\:flex-row { flex-direction: row; }
.md\:grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.md\:grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
.md\:text-lg { font-size: 1.125rem; }
}
/* Container queries */
@container (min-width: 400px) {
.@container\:grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
}
}
Variants
CUBE CSS methodology
/* CUBE CSS — Composition, Utility, Block, Exception */
/* Composition — layout patterns */
.stack {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.cluster {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
/* Utility — single purpose */
.bg-brand-500 { background-color: var(--color-brand-500); }
.text-white { color: white; }
.p-4 { padding: 1rem; }
/* Block — component */
.product-card { /* ... */ }
/* Exception — state modifiers */
.product-card:where(.is-featured) {
border-color: var(--color-brand-500);
}
Atomic CSS con UnoCSS
<!-- UnoCSS — atomic classes generated on demand -->
<div class="flex flex-col items-center gap-4 p-6 bg-white rounded-lg shadow-md">
<h2 class="text-xl font-bold text-gray-900">Title</h2>
<p class="text-sm text-gray-600">Description</p>
</div>
Best Practices
-
For a deeper guide, see Container-Presenter: Separate Data Logic from Rendering.
-
Usá
@layerpara cascade control — declará@layer base, components, utilitiespara que utilities siempre ganen -
Mantené design tokens en
:root— colors, spacing, fonts, shadows como CSS custom properties -
Usá
@applycon moderación — es útil para component classes pero no buildes apps enteras con eso -
Preferí utility classes para layout — flex, grid, padding, margin son más fast como utilities
-
Usá component classes para complex patterns — buttons, cards, inputs que tienen hover/focus states
-
Scopéa component styles con CSS modules — previene class name collisions en apps grandes
-
Usá semantic token names —
--color-primaryno--color-blue-500, para que podás rethemear sin cambiar usages -
Supportá dark mode con token overrides — cambiá token values en un media query, no components individuales
Common Mistakes
- No cascade layers: utilities y component styles pelean por specificity.
@layerlo resuelve declarativamente. - Hardcodear values:
color: #3b82f6en vez decolor: var(--color-brand-500). Los changes require find-and-replace. - Over-usar
@apply: convertir cada utility combination en un component class. Esto defeat el purpose de utilities. - No usar CSS modules para components: global class names como
.buttoncolisionan. Usá.module.csso scoped styles. - Ignorar dark mode: buildar light mode only y boltar on dark mode después. Design tokens desde el start lo hacen trivial.
FAQ
¿Qué es el utility-first CSS approach?
Escribir styles usando classes chicas y single-purpose como flex, p-4, text-center en vez de custom CSS. Tailwind CSS es el utility-first framework más popular. Componés utilities directamente en HTML/JSX.
¿Qué son CSS layers?
CSS @layer provee explicit cascade control. Los styles en layers posteriores overridean los anteriores regardless de specificity. Declará @layer base, components, utilities para que utilities siempre overrideen component y base styles.
¿Debería usar CSS modules o Tailwind?
Ambos. Usá Tailwind utilities para layout y spacing (fast, consistent). Usá CSS modules para complex component styles con hover, focus, y state transitions (scoped, maintainable).
¿Qué son design tokens?
CSS custom properties que storean design decisions: colors, fonts, spacing, shadows, radii. Centralizan el design system para que los changes propaguen everywhere. Ejemplo: --color-primary: var(--color-brand-500).
¿Cómo manejo dark mode?
Overrideá design tokens en un @media (prefers-color-scheme: dark) block. Cambiá --color-bg, --color-text, etc. Los components automáticamente pick up los new values sin changes al component CSS.
Recursos Relacionados
Container-Presenter: Separate Data Logic from Rendering
How to separate data-fetching logic from rendering in React using the container-presenter pattern. Covers hooks migration, testing benefits, and trade-offs.
PatternIslands Architecture
How to ship interactivity only where needed using islands architecture. Covers Astro islands, partial hydration, React islands, and performance benefits.
PatternProgressive Enhancement
How to build a functional HTML baseline and progressively enhance with JavaScript. Covers core functionality, feature detection, graceful degradation, and accessibility.