10 Essential Lessons for Structuring CSS After Tailwind
After eight years of happily using Tailwind CSS to build dozens of small websites, I recently embarked on a migration back to semantic HTML paired with vanilla CSS. What started as a daunting task turned into a fascinating journey—one that revealed how much Tailwind had secretly taught me about structuring stylesheets. As a non-full-time front-end developer who learned CSS in fits and starts, I discovered that many of Tailwind's internal systems could be replicated with custom approaches. Below are the ten key insights I gained while moving away from utility-first CSS and learning to write clean, maintainable vanilla CSS.
1. Reset: Building on Solid Ground
Tailwind's "preflight" reset is one of its most invisible yet valuable features. When I stripped away Tailwind, I realized how many base rules I'd taken for granted—such as * { box-sizing: border-box; } and html { line-height: 1.5; }. Rather than starting from scratch, I simply copied the first 200 lines of Tailwind's preflight CSS into my own reset file. This gave me a familiar foundation without any bloat. The reset ensures consistent spacing, removes browser defaults, and sets a neutral baseline. It's the first layer of any well-structured CSS codebase, and Tailwind's version is an excellent starting point even when you're writing custom styles.
2. Components: Organizing CSS by Self-Contained Units
The bulk of my CSS now lives in component-specific files. Each component gets a unique class, and its styles never leak into other components. For example, a button component has its own .button class with all related properties—hover states, sizes, colors—defined within that file. This approach mirrors the mental model of Vue or React components, even if no JavaScript framework is involved. The key rule: CSS for one component never overrides another. This isolation prevents accidental cascading bugs and makes it easy to edit one component without fear of breaking something elsewhere. Typically, 80% of the CSS changes you'll make will happen inside these component files.
3. Colors: Designing a Reusable Palette
Tailwind's color palette (e.g., red-500, blue-600) is incredibly useful because it provides a consistent, semantic scale. When moving away, I created my own set of CSS custom properties for colors—--color-primary, --color-accent, --color-gray-100 through --color-gray-900. This gives me the same flexibility and consistency without the utility classes. By using custom properties, I can change the entire site's color scheme by editing just a few variables. It also forces me to think about design tokens rather than inline hex values, which leads to more cohesive visuals.
4. Font Sizes: Establishing a Typographic Scale
Tailwind's font-size utilities like text-lg and text-2xl follow a predefined modular scale. In my vanilla CSS, I replicated this by defining a set of font-size custom properties: --font-size-sm, --font-size-base, --font-size-lg, and so on. This scale ensures that all text sizes are proportional and harmonious. Instead of guessing font-size: 1.25rem for each heading, I use the scale. It's a small system that prevents typographic chaos. I even keep the same ratios as Tailwind's default scale because they work well across devices.
5. Utility Classes: Keeping Only What You Need
Yes, even after moving away from Tailwind, I still use a handful of utility classes. These are single-purpose classes like .text-center, .mt-4, or .flex. The difference is that I define them manually in a dedicated utilities file, and I only create ones I actually use. Tailwind's biggest advantage is its massive collection of utilities—but that can also be its biggest downside (bloat). By curating your own set, you get the convenience of inline helpers without the overhead. For example, I keep a .sr-only class for screen-reader-only content and .hidden for display: none.
6. The Base Layer: Setting Core Element Styles
Beyond the reset, there's a "base" layer that defines default styles for HTML elements: headings (h1–h6), paragraphs, links, lists, and form elements. This is where I set things like h1 { font-size: var(--font-size-2xl); font-weight: 700; }. Tailwind's preflight already normalizes some of this, but base styles go further to establish a visual rhythm. I keep this file thin—only a few dozen lines—because most specifics live in component files. The base layer is about defaults, not overrides.
7. Spacing: Using a Consistent System
Tailwind's spacing scale (p-4, m-8), based on a 4px increment, is genius. For my vanilla CSS, I created custom properties for spacing: --space-xs (4px), --space-sm (8px), --space-md (16px), --space-lg (32px), etc. Every margin, padding, and gap in my components references these variables. This ensures that vertical rhythm and white space remain consistent across the entire site. It also makes responsive design easier: I can change the root font size and spacing scales proportionally.
8. Responsive Design: Using Media Queries with Purpose
Tailwind's responsive prefixes (md:flex, lg:text-lg) are convenient, but I found that media queries in vanilla CSS are just as effective when organized well. I now group media queries for a component within its own file, using standard breakpoints based on content (e.g., 48em, 64em). To avoid repetition, I use custom properties for breakpoint names, like @media (min-width: 48em) { ... }. This keeps CSS readable and doesn't require any build tools. The key insight is that responsive design is not about adding utilities but about thinking in terms of layout shifts at specific viewport widths.
9. The Build System: Keeping It Simple
When I used Tailwind, I relied on its CLI for processing and purging unused styles. For vanilla CSS, I switched to a minimal build pipeline: one that concatenates my files (reset, base, utilities, components) and minifies the output. I use a simple Node script instead of a full bundler. This reduces complexity and speeds up development. The important part is that I still get benefits like autoprefixing and CSS nesting (via PostCSS) without the overhead of Tailwind's JIT engine. The result is a lean, fast build process that I fully understand.
10. Lessons Learned: Tailwind Taught Me More Than I Knew
Stepping away from Tailwind forced me to formalize the systems I had internalized. I learned that Tailwind's real value isn't the utility classes themselves—it's the consistent design decisions encoded in those classes. By extracting those decisions into custom properties, component files, and a clean reset, I built a CSS architecture that's just as maintainable but far more transparent. I now have full control over every line of CSS, and I understand why each rule exists. The migration wasn't about rejecting Tailwind; it was about growing as a developer and learning to structure CSS with intention.
In conclusion, moving away from Tailwind to vanilla CSS is not a step backward—it's a step toward deeper understanding. By adopting systems for resets, components, colors, spacing, and more, you can achieve the same consistency and speed Tailwind offers, but with complete ownership. Start small, mimic the parts you love, and gradually build your own framework. The result is a codebase that's both familiar and uniquely yours.