Mi Problema 🤔
Mi portfolio llevaba meses funcionando perfectamente con Tailwind CSS. Todo iba bien. Los estilos estaban en su sitio, los componentes se veían geniales y la web cargaba rápido. Entonces, ¿por qué cambiar? Porque funcionar bien y funcionar de la mejor manera posible son cosas distintas. Cuando te paras a analizar qué hay debajo del capó, a veces descubres que llevas una capa que no necesitas.
No me malinterpretéis: Tailwind CSS es una herramienta brutal. Lo he usado en proyectos profesionales y lo seguiré usando donde tenga sentido. Pero en un portfolio personal con Astro, empecé a notar ciertas cosas:
- Dependencia innecesaria: 3 paquetes extra (tailwindcss, @tailwindcss/vite, @tailwindcss/typography) para un proyecto que no los necesitaba realmente
- Capa de abstracción: Tailwind genera CSS a partir de clases de utilidad. Es una capa entre lo que escribes y lo que el navegador interpreta. En un proyecto pequeño, esa capa suma sin aportar
- Peso extra: El CSS generado incluía utilidades que no siempre aprovechaba al máximo. ~20KB de más que el usuario descargaba sin necesidad
- Menor control: Cuando quieres algo muy específico, acabas luchando contra el framework en vez de escribir directamente lo que necesitas
Mi Solución 🧩
La idea era sencilla: eliminar Tailwind y sustituirlo por CSS vanilla con un sistema de design tokens usando custom properties de CSS.
¿Qué son los design tokens? Son variables CSS que definen tu sistema de diseño:
:root {
--color-primary: oklch(0.55 0.2 260);
--color-gray-100: oklch(0.97 0 0);
--color-gray-900: oklch(0.21 0.006 285.75);
--text-sm: 0.875rem;
--text-base: 1rem;
--text-xl: 1.25rem;
--spacing: 0.25rem;
--radius-lg: 0.5rem;
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--transition-colors: color 0.15s, background-color 0.15s, border-color 0.15s;
}
Con esto tengo consistencia en todo el proyecto sin depender de ningún framework. Solo CSS puro que el navegador entiende directamente.
El proceso fue migrar 38 archivos en un solo commit. Cada componente Astro pasó de usar clases de Tailwind a tener su propio bloque <style> con estilos scoped:
Antes (Tailwind):
<header class="flex items-center justify-between px-6 py-4 bg-white dark:bg-gray-900">
<nav class="flex gap-4">
<a class="text-sm font-medium text-gray-700 hover:text-blue-500">Blog</a>
</nav>
</header>
Después (Vanilla CSS):
<header class="header">
<nav class="nav">
<a class="nav-link">Blog</a>
</nav>
</header>
<style>
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: calc(var(--spacing) * 4) calc(var(--spacing) * 6);
background-color: var(--color-white);
}
:global(.dark) .header {
background-color: var(--color-gray-900);
}
.nav {
display: flex;
gap: calc(var(--spacing) * 4);
}
.nav-link {
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-gray-700);
transition: var(--transition-colors);
}
.nav-link:hover {
color: var(--color-blue-500);
}
</style>
¿Más líneas? Sí. ¿Más claro y mantenible? Absolutamente.
La migración no fue solo “quitar Tailwind y poner CSS”. Hubo trampas interesantes que vale la pena compartir.
Estilos scoped y <slot>
En Astro, los estilos dentro de <style> son scoped por defecto. Esto significa que cada componente recibe un atributo único (data-astro-cid-*) y los estilos solo afectan a ese componente.
El problema: el contenido que llega por <slot> no recibe ese atributo. Si un componente padre intenta estilizar lo que viene por slot, los estilos no se aplican.
La solución: usar :global() para los selectores que afectan a contenido slotted:
.container :global(a) {
color: var(--color-primary);
text-decoration: underline;
}
opacity vs transparencia de fondo
Con Tailwind usaba clases como bg-opacity-70. Al migrar, el primer instinto fue usar la propiedad opacity. Error: opacity afecta al elemento entero, incluidos sus hijos.
La solución correcta: color-mix() para transparencia solo en el fondo:
.modal-overlay {
/* MAL: afecta a todo */
opacity: 0.7;
/* BIEN: solo el fondo es transparente */
background-color: color-mix(in oklab, var(--color-gray-900) 70%, transparent);
}
Mi Resultado 🎯
Los números hablan por sí solos:
- ~20KB menos de CSS entregado al navegador
- 3 dependencias eliminadas del package.json
- 0 capas de abstracción entre tu código y el navegador
- Build más rápido al no necesitar el procesamiento de Tailwind
- Control total sobre cada línea de CSS que se genera
El package.json pasó de tener tailwindcss, @tailwindcss/vite y @tailwindcss/typography a no tener ninguna dependencia de estilos. Solo CSS puro. Y lo mejor: se eliminó el archivo tailwind.config.mjs por completo. Una configuración menos que mantener.
El sistema de estilos quedó organizado en 4 archivos CSS:
| Archivo | Responsabilidad |
|---|---|
| design-tokens.css | Colores, tipografía, espaciado, sombras, transiciones |
| base-reset.css | Reset CSS mínimo y utilidades como .sr-only |
| prose.css | Tipografía para contenido del blog con soporte dark mode |
| layout.css | Clases de layout globales (.bodyLayout, .mainLayout) |
Todo importado desde un único global.css. Limpio, predecible y sin magia negra.
Migrar de Tailwind CSS a vanilla CSS no es para todos ni para todos los proyectos. En equipos grandes o proyectos con muchos desarrolladores, Tailwind sigue siendo una opción fantástica por su consistencia y velocidad de desarrollo. Pero en un proyecto personal como un portfolio con Astro, donde el rendimiento importa y el control total es un lujo que puedes permitirte, quitarte esa capa de abstracción es liberador. De hecho, acabé llevando esta filosofía aún más lejos y migré todo el portfolio de Astro a Swift — cuento la historia completa en Astro to Saga.
Es como pintar un cuadro: puedes usar plantillas y herramientas que te guíen, o puedes coger el pincel y crear exactamente lo que tienes en mente. Ambas opciones son válidas. Pero cuando el lienzo es tuyo, pintar a mano tiene su encanto.
Keep coding, keep running 🏃♂️