Back to Blog
jacken@blog:~$ cat modern-css-patterns-tailwind-to-css-in-js.md

Modern CSS Patterns: From Tailwind to CSS-in-JS

December 6, 20258 min readby Jacken Holland
CSSTailwindCSS-in-JSWeb DevelopmentFrontend

The CSS ecosystem has consolidated significantly in 2025. After using virtually every styling approach in production—from Tailwind v4 to vanilla-extract to plain CSS with modern features—I've developed strong opinions on when to reach for each tool.

Let me share what's actually working in real projects, not theoretical comparisons.

Tailwind v4: The Maturity Milestone

Tailwind v4, released in mid-2024 and refined throughout 2025, fundamentally changed the framework. The new engine is dramatically faster, the configuration is simpler, and the CSS output is cleaner.

What Changed in v4

The biggest shift: Tailwind v4 is now built on a lightning-fast Rust engine (Oxide). My build times dropped from 800ms to under 100ms. For large projects, this matters—you notice the difference on every save.

The configuration approach also simplified. Instead of a complex JavaScript config file, you now define customizations in CSS:

@import "tailwindcss";

@theme {
  --color-brand-50: #f0f9ff;
  --color-brand-900: #0c4a6e;

  --spacing-18: 4.5rem;
  --spacing-88: 22rem;

  --font-size-xxs: 0.625rem;
}

This feels more natural and enables better tooling support. My editor now autocompletes custom design tokens, which wasn't possible with the JS config.

When Tailwind v4 Excels

I reach for Tailwind v4 on projects where:

  1. I'm prototyping quickly - No context switching between files
  2. Design system with constraints - The design token approach enforces consistency
  3. Team collaboration - Everyone can read utility classes without learning CSS deeply
  4. Component libraries - Works beautifully with React, Vue, Svelte

The classic Tailwind component pattern still works perfectly:

export function ProductCard({ product }) {
  return (
    <article className="group relative overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm transition-shadow hover:shadow-xl">
      <div className="aspect-square overflow-hidden bg-gray-100">
        <img
          src={product.image}
          alt={product.name}
          className="h-full w-full object-cover transition-transform group-hover:scale-105"
        />
      </div>

      <div className="p-4">
        <h3 className="text-lg font-semibold text-gray-900">
          {product.name}
        </h3>
        <p className="mt-1 text-sm text-gray-600 line-clamp-2">
          {product.description}
        </p>

        <div className="mt-4 flex items-center justify-between">
          <span className="text-xl font-bold">${product.price}</span>
          <button className="btn-primary">Add to Cart</button>
        </div>
      </div>
    </article>
  );
}

The hover effects, responsive sizing, and focus states are all handled with utilities. No CSS file needed.

Using AI to Generate Tailwind Components

I use AI assistants extensively for Tailwind component generation:

Prompt I use frequently:

"Create a Tailwind v4 component for a pricing card. Include three tiers (Basic, Pro, Enterprise) with different visual weights. Use the group hover pattern for subtle interactions. Follow 2025 design trends with good contrast and accessibility."

The AI generates clean, accessible Tailwind markup that I can refine. It's dramatically faster than building from scratch.

Where Tailwind Falls Short

I've learned not to use Tailwind for:

  • Complex animations - CSS/JS animations are more flexible
  • Highly custom designs - Fighting the utility system becomes painful
  • Print stylesheets - Utilities don't map well to print media
  • Legacy browser support - If you need IE11, Tailwind v4 isn't the answer

CSS Modules: The Reliable Middle Ground

CSS Modules remain my choice for projects where Tailwind feels too constraining but full CSS-in-JS is overkill.

The Pattern That Works

I use CSS Modules with composition for shared styles:

/* components/Button.module.css */
.base {
  padding: 0.75rem 1.5rem;
  border-radius: 0.5rem;
  font-weight: 500;
  transition: all 0.2s;
}

.primary {
  composes: base;
  background: var(--color-blue-600);
  color: white;
}

.secondary {
  composes: base;
  background: var(--color-gray-200);
  color: var(--color-gray-900);
}
import styles from './Button.module.css';

export function Button({ variant = 'primary', children }) {
  return (
    <button className={styles[variant]}>
      {children}
    </button>
  );
}

The composition feature eliminates duplication while keeping styles scoped. It's the best of both worlds.

Modern CSS Features in Modules

With CSS Modules, I can use modern CSS features directly:

Container Queries:

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

.card-content {
  padding: 1rem;
}

@container (min-width: 400px) {
  .card-content {
    padding: 2rem;
    display: grid;
    grid-template-columns: 1fr 2fr;
  }
}

Cascade Layers:

@layer components {
  .button {
    /* Component styles */
  }
}

@layer utilities {
  .text-center {
    text-align: center;
  }
}

These features work perfectly in CSS Modules and give you control that utility frameworks can't match.

CSS-in-JS in 2025: Survival of the Fittest

The CSS-in-JS landscape consolidated significantly. styled-components and Emotion are stable but not actively pushing boundaries. The exciting movement is in zero-runtime solutions.

Vanilla Extract: My Go-To for Design Systems

For component libraries and design systems, I use vanilla-extract. It's CSS-in-TS with zero runtime overhead.

// button.css.ts
import { style } from '@vanilla-extract/css';

export const button = style({
  padding: '0.75rem 1.5rem',
  borderRadius: '0.5rem',
  fontWeight: 500,
  transition: 'all 0.2s',

  ':hover': {
    transform: 'translateY(-1px)',
  }
});

export const primary = style([button, {
  background: 'var(--color-blue-600)',
  color: 'white',
}]);

The killer feature: full TypeScript support for CSS. Typos in property names? Compile error. Invalid values? Compile error.

Panda CSS: The Newcomer Worth Watching

Panda CSS emerged in late 2024 and gained traction throughout 2025. It's a hybrid approach that combines the best of Tailwind and CSS-in-JS.

import { css } from '../styled-system/css';

export function Button() {
  return (
    <button className={css({
      bg: 'blue.600',
      color: 'white',
      px: '6',
      py: '3',
      rounded: 'md',
      _hover: { bg: 'blue.700' }
    })}>
      Click me
    </button>
  );
}

It generates atomic CSS at build time, giving you Tailwind-like performance with the developer experience of CSS-in-JS. I'm using it on a few projects and it's promising.

Plain Modern CSS: Underrated in 2025

With native CSS nesting, container queries, and cascade layers, plain CSS has caught up to preprocessors. For simple projects, I now skip the tooling entirely.

CSS Nesting (Native)

.card {
  padding: 1rem;
  background: white;

  & .title {
    font-size: 1.5rem;
    font-weight: bold;
  }

  & .description {
    margin-top: 0.5rem;
    color: gray;
  }

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

No Sass needed. This is native CSS in all modern browsers.

Custom Properties for Theming

I use CSS custom properties for runtime theming:

:root {
  --color-primary: #3b82f6;
  --color-secondary: #6b7280;
  --spacing-unit: 0.25rem;
}

[data-theme="dark"] {
  --color-primary: #60a5fa;
  --color-secondary: #9ca3af;
}

.button {
  background: var(--color-primary);
  padding: calc(var(--spacing-unit) * 3) calc(var(--spacing-unit) * 6);
}

Switching themes is just toggling a data attribute. No JavaScript required.

My Decision Framework

Here's how I choose a styling approach for new projects in 2025:

Use Tailwind v4 if:

  • Building an MVP or prototype quickly
  • Working with a team that prefers utility classes
  • Want strong design system constraints
  • Using a component framework (React, Vue, Svelte)

Use CSS Modules if:

  • Need more styling flexibility than Tailwind provides
  • Want scoped styles without runtime overhead
  • Using modern CSS features (container queries, cascade layers)
  • Prefer writing actual CSS

Use vanilla-extract if:

  • Building a component library or design system
  • Want TypeScript validation for styles
  • Need zero runtime overhead
  • Comfortable with build-time CSS generation

Use plain CSS if:

  • Building a simple site with minimal styling needs
  • Want zero build tooling
  • Leveraging modern CSS features
  • Don't need scoped styles

Use Panda CSS if:

  • Want Tailwind-like DX with better TypeScript support
  • Building a Next.js or React app
  • Need atomic CSS with zero runtime
  • Experimenting with newer tools

AI-Assisted Styling Workflows

I use AI assistants to convert between styling approaches:

Conversion Prompt:

"Convert this Tailwind component to vanilla-extract. Maintain the same visual appearance and hover effects. Use TypeScript and the latest vanilla-extract patterns."

This speeds up migrations and helps me learn new styling approaches by seeing equivalent implementations.

Performance Considerations

After auditing dozens of sites, here's what actually impacts performance:

  1. CSS bundle size - Purge unused styles (Tailwind does this automatically)
  2. Critical CSS - Inline above-fold styles for faster FCP
  3. CSS-in-JS runtime - Zero-runtime solutions (vanilla-extract, Panda) beat runtime solutions (styled-components)
  4. Specificity conflicts - Cascade layers solve this elegantly

The difference between well-optimized Tailwind and well-optimized CSS Modules is marginal. Choose based on developer experience, not minor performance differences.

Tooling I Use Daily

Tailwind CSS IntelliSense: Essential VS Code extension for Tailwind CSS Modules TypeScript: Type generation for CSS Module class names PostCSS with postcss-preset-env: Use modern CSS features today Lightning CSS: Faster CSS processing than PostCSS (I'm migrating)

The Future: CSS is Getting Better

Looking ahead to 2026, I'm excited about:

View Transitions API: Native page transitions without JavaScript @scope rule: Better style encapsulation without CSS-in-JS Color functions: oklch() and color-mix() for better color manipulation Subgrid: Grid layout for nested components

Modern CSS is maturing to the point where we might not need as much tooling.

What I've Learned

The CSS wars are over. There's no single "correct" approach—just trade-offs you make based on project requirements.

I use Tailwind v4 for rapid prototyping, CSS Modules for production apps with complex styling, vanilla-extract for component libraries, and plain modern CSS for simple sites.

The key is understanding the strengths and limitations of each approach. Don't force Tailwind into every project, and don't avoid it on principle.

Use the right tool for the job, and ship great-looking, performant interfaces.