← EasyTool.me

Moving Away from Tailwind CSS: Building Maintainable CSS Architecture with Vanilla CSS

Published: 2026-05-17 Reading: 8 min CSS Architecture / Tailwind CSS / Vanilla CSS / Frontend

Programmer Julia Evans recently shared her experience migrating a project from Tailwind CSS to vanilla CSS on Hacker News. The post earned 379 points and 244 comments, sparking a heated discussion in the frontend community. She used Tailwind for 8 years before making the switch — not because Tailwind is bad, but because she had learned enough from it to build a better system with vanilla CSS.

This guide breaks down her methodology with practical code examples. If you're considering moving away from Tailwind CSS, or just want to understand better CSS architecture patterns, this article is for you.

Why Move Away from Tailwind CSS

Tailwind CSS has been one of the most popular CSS frameworks in recent years. Its atomic design philosophy lets developers assemble interfaces quickly using utility classes without writing custom CSS. But as projects grow, several pain points emerge:

Julia's core argument: Tailwind is excellent for learning CSS and building quick prototypes, but once you truly understand CSS design principles, vanilla CSS can produce cleaner, more maintainable code.

What Tailwind Taught Us

Before leaving Tailwind behind, Julia makes an important point: Tailwind is an excellent CSS teaching tool. It taught many developers these critical concepts:

These design principles, learned from Tailwind, can be directly carried over to vanilla CSS. The key is to codify them using CSS custom properties.

Building a Design System with CSS Variables

One of vanilla CSS's greatest strengths is custom properties (variables). Julia's approach is to rebuild Tailwind's design system using CSS variables:

Color Variables

Defined on :root for global access:

:root {
  /* Primary colors */
  --pink: #fea0c2;
  --blue: #a0d0fe;
  --green: #a0fea0;
  --yellow: #fefe80;
  --purple: #d0a0fe;

  /* Neutral colors */
  --gray-50: #f9fafb;
  --gray-100: #f3f4f6;
  --gray-200: #e5e7eb;
  --gray-500: #6b7280;
  --gray-700: #374151;
  --gray-900: #111827;

  /* Semantic colors */
  --text: var(--gray-900);
  --text-muted: var(--gray-500);
  --bg: #ffffff;
  --bg-subtle: var(--gray-50);
  --border: var(--gray-200);
}

The benefit: change once, apply everywhere. Want dark mode support? Just redefine these variables inside @media (prefers-color-scheme: dark).

Type Scale

Directly ported from Tailwind's font size system:

:root {
  --size-xs: 0.75rem;    /* 12px */
  --size-sm: 0.875rem;   /* 14px */
  --size-base: 1rem;     /* 16px */
  --size-lg: 1.125rem;   /* 18px */
  --size-xl: 1.25rem;    /* 20px */
  --size-2xl: 1.5rem;    /* 24px */
  --size-3xl: 1.875rem;  /* 30px */
  --size-4xl: 2.25rem;   /* 36px */
}
💡 Tip: Use --size-xs instead of --font-size-xs for brevity. Your team only needs to remember one naming prefix.

Spacing System

:root {
  --space-1: 0.25rem;   /* 4px */
  --space-2: 0.5rem;    /* 8px */
  --space-3: 0.75rem;   /* 12px */
  --space-4: 1rem;      /* 16px */
  --space-6: 1.5rem;    /* 24px */
  --space-8: 2rem;      /* 32px */
  --space-12: 3rem;     /* 48px */
  --space-16: 4rem;     /* 64px */
}

CSS Component Architecture

This is the most important part of Julia's methodology. She borrows the component idea from Vue/React but implements it in pure CSS: each component has a unique class name and its own CSS file.

Design Principles

File Structure

css/
  reset.css           /* CSS Reset */
  variables.css       /* Global CSS variables */
  base.css            /* Base styles */
  components/
    site-header.css   /* Header component */
    blog-card.css     /* Blog card */
    search-box.css    /* Search input */
    footer.css        /* Footer */
  utilities.css       /* Utility classes */

Component Example: Blog Card

HTML:

<article class="blog-card">
  <img class="blog-card__image" src="cover.jpg" alt="Cover image">
  <div class="blog-card__body">
    <h3 class="blog-card__title">Article Title</h3>
    <p class="blog-card__excerpt">Article summary...</p>
    <span class="blog-card__date">2026-05-17</span>
  </div>
</article>

CSS (components/blog-card.css):

.blog-card {
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  background: var(--bg);
  transition: box-shadow 0.2s;
}

.blog-card:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.blog-card__image {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

.blog-card__body {
  padding: var(--space-4);
}

.blog-card__title {
  font-size: var(--size-xl);
  font-weight: 600;
  margin: 0 0 var(--space-2);
}

.blog-card__excerpt {
  font-size: var(--size-sm);
  color: var(--text-muted);
  margin: 0 0 var(--space-3);
}

.blog-card__date {
  font-size: var(--size-xs);
  color: var(--text-muted);
}

Notice: .blog-card__title and .blog-card__excerpt can never conflict because they're scoped under the .blog-card namespace. This is far clearer than Tailwind's class="text-xl font-semibold mb-2".

Minimal Base Styles

Julia keeps base styles extremely lean — only the defaults that are truly needed globally:

/* reset.css - Simplified CSS Reset */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
}

body {
  font-family: 'Inter', system-ui, sans-serif;
  font-size: var(--size-base);
  line-height: 1.6;
  color: var(--text);
  background: var(--bg);
}

img {
  max-width: 100%;
  display: block;
}

a {
  color: var(--blue);
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

section {
  padding: var(--space-8) 0;
}

No excess. No global font-size overrides. No complex reset rules. Just enough.

Managing Spacing with the Owl Selector

This is the most elegant part of Julia's methodology. The owl selector (* + *), proposed by Heydon Pickering, solves vertical spacing inside components with a single line of CSS:

section > * + * {
  margin-top: 1rem;
}

What it means: any element inside a section that isn't the first child gets a margin-top. No need to add spacing to each child element individually, no extra wrappers, no gap properties.

Compared to Tailwind

Tailwind version:

<section class="flex flex-col gap-4">
  <h2>Title</h2>
  <p>Paragraph</p>
  <div>Content</div>
</section>

Owl selector version:

<section>
  <h2>Title</h2>
  <p>Paragraph</p>
  <div>Content</div>
</section>

Cleaner HTML, and spacing logic lives entirely in CSS. You can even define different spacing per section:

.hero-section > * + * {
  margin-top: var(--space-6);
}

.blog-list > * + * {
  margin-top: var(--space-4);
}

.footer > * + * {
  margin-top: var(--space-2);
}
⚠️ Heads up: The owl selector targets all children, including elements you might not want to space (like absolutely-positioned decorations). Override those with margin-top: 0.

Container Queries: Truly Responsive Design

Tailwind's responsive design relies on media queries, which are based on viewport width — not the actual available space of a component. CSS container queries solve this:

.blog-card {
  container-type: inline-size;
  container-name: card;
}

/* Switch to horizontal layout when card container >= 400px */
@container card (min-width: 400px) {
  .blog-card {
    display: grid;
    grid-template-columns: 200px 1fr;
  }

  .blog-card__image {
    height: 100%;
  }
}

This means the same blog card component automatically switches to a compact horizontal layout in a sidebar, and a spacious vertical layout in the main content area — no JavaScript needed, no props from parent components.

Container Queries vs Media Queries

/* Media query: based on viewport width, imprecise */
@media (min-width: 768px) {
  .blog-card { /* Active when viewport >= 768px */ }
}

/* Container query: based on component's actual width, precise */
@container card (min-width: 400px) {
  .blog-card { /* Active when card >= 400px wide */ }
}

All modern browsers support container queries (Chrome 105+, Firefox 110+, Safari 16+). Safe for production use today.

Keeping Essential Utilities

Julia doesn't completely abandon utility classes. She keeps a small set of truly useful global utilities:

/* Accessibility: visually hidden but readable by screen readers */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

/* Text truncation */
.truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Container */
.container {
  max-width: 1200px;
  margin-inline: auto;
  padding-inline: var(--space-4);
}

These are genuinely cross-component utilities — few in number, clear in purpose. Completely different from Tailwind's hundreds of utility classes.

Migration Guide

If you're considering moving away from Tailwind CSS, here's a step-by-step approach:

Step 1: Establish Your Variable System

Extract colors, fonts, spacing, and other design tokens from your existing tailwind.config.js and convert them to CSS variables. This can be semi-automated with a script that parses the Tailwind config and generates variables.css.

Step 2: Start Small

Don't replace everything at once. Pick one or two small components (a button, a card) and rewrite them in vanilla CSS while keeping the rest on Tailwind.

Step 3: Set Up Your File Structure

Organize your CSS files using the component architecture described above. One file per component for easy maintenance and discovery.

Step 4: Incremental Replacement

Each time you modify a page or component, replace its Tailwind classes with vanilla CSS. This makes migration gradual and limits the risk of introducing bugs.

Step 5: Remove Tailwind

Once all components are migrated, remove Tailwind from your project dependencies. Check your build config to ensure CSS files are properly bundled.

💡 Helpful tools: During migration, you can use online developer tools to check CSS syntax, validate color contrast ratios, and test responsive layouts.

Conclusion

Julia Evans' experience teaches an important lesson: good tools don't have to be permanent companions. Tailwind CSS is an excellent CSS teaching tool and rapid prototyping framework, but once you truly understand CSS design philosophy, vanilla CSS gives you greater flexibility and better long-term maintainability.

Key takeaways:

This isn't an argument that Tailwind CSS is bad — it remains an excellent choice for quick prototypes and small projects. But for medium-to-large projects that need long-term maintenance, mastering vanilla CSS architecture is a skill worth investing in.

Further reading: Julia Evans' original post New CSS things I learned, and Heydon Pickering's Lobotomized Owl selector article.