Tailwind to CSS

Jorge pintándose a sí mismo en un lienzo, representando el control total sobre los estilos de su portfolio.

Tabla de contenidos


🎨 ¿Por Qué Tocar Lo Que Funciona?

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. Y cuando te paras a analizar qué hay debajo del capó, a veces descubres que llevas una capa que no necesitas 🧅.


🤔 El Problema con Tailwind (en mi caso)

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 ⚔️

💡 La Decisión: Vanilla CSS con Design Tokens

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 tienes consistencia en todo el proyecto sin depender de ningún framework. Solo CSS puro que el navegador entiende directamente 🎯.


🔧 La Migración

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 ✅.


📚 Lo Que Aprendí por el Camino

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);
}

📊 Los Resultados

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 🧹.


🏗️ La Arquitectura Final

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 🧙‍♂️.


💭 Reflexión Final

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 🏃‍♂️